mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
Compare commits
133 Commits
xo6/v2v-im
...
2ac7c0ffb8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ac7c0ffb8 | ||
|
|
68ef82a9f3 | ||
|
|
565dbfa835 | ||
|
|
a8952875ac | ||
|
|
7b44a15a51 | ||
|
|
22ed4ac652 | ||
|
|
ee90cea718 | ||
|
|
1f2469b711 | ||
|
|
533f69244b | ||
|
|
fbcf5ad7f7 | ||
|
|
104cdfbcb1 | ||
|
|
574e427b0a | ||
|
|
48b445211b | ||
|
|
a54e37c808 | ||
|
|
48ad080ada | ||
|
|
210e4f124e | ||
|
|
e7da1be6ba | ||
|
|
d10aa60358 | ||
|
|
374d2c9d39 | ||
|
|
b82d641783 | ||
|
|
3a41fba148 | ||
|
|
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 | ||
|
|
422512514a | ||
|
|
ee217dbf45 | ||
|
|
f6f5f05696 | ||
|
|
1acd6533fc | ||
|
|
12cf925a89 | ||
|
|
d550e4356b | ||
|
|
03fc6b5b1a | ||
|
|
5d79b4486e | ||
|
|
2ea5887ad1 | ||
|
|
9fc2dc2b7c | ||
|
|
c89eab12bf | ||
|
|
d63f6a794c | ||
|
|
2739715642 | ||
|
|
26eadbd6a4 | ||
|
|
79fff5af37 | ||
|
|
8be55f783b | ||
|
|
2655842321 | ||
|
|
f593012837 | ||
|
|
a2f590be31 | ||
|
|
bcecc5ded0 | ||
|
|
7fe262fbbe | ||
|
|
dafc829cee | ||
|
|
8ea46eb530 | ||
|
|
9e59db67ee | ||
|
|
73694a0da6 | ||
|
|
6c201f2117 | ||
|
|
3be9f3e94a | ||
|
|
b1741f44f5 | ||
|
|
ffd581def2 | ||
|
|
12cc94e98b | ||
|
|
d80a5f52f0 | ||
|
|
da9044c98e | ||
|
|
ba01b16d1a | ||
|
|
3687817765 | ||
|
|
8bd11632f0 | ||
|
|
b2fd9180b1 | ||
|
|
8c00429922 | ||
|
|
3a1191dfe1 | ||
|
|
9336c1c8ac | ||
|
|
a3bacc45a3 | ||
|
|
de2cfc864d | ||
|
|
dfa0baf1fa | ||
|
|
501bb71dc1 | ||
|
|
de1d6ed21d | ||
|
|
7d4f325330 | ||
|
|
f1b316bd01 | ||
|
|
166c4d1e46 | ||
|
|
a2c83b402f | ||
|
|
d4047e3b9b |
@@ -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
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<NetworkFormTextInput v-bind="nameInputBindings" />
|
||||
<NetworkFormNumberInput v-bind="mtuInputBindings" />
|
||||
<VtsNumberInput v-bind="mtuInputBindings" />
|
||||
</div>
|
||||
<NewNetworkDescriptionTextarea v-bind="descriptionInputBindings" />
|
||||
</div>
|
||||
@@ -18,7 +18,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import NetworkFormNumberInput from '@/modules/network/components/form/new/inputs/NetworkFormNumberInput.vue'
|
||||
import NetworkFormSelect from '@/modules/network/components/form/new/inputs/NetworkFormSelect.vue'
|
||||
import NetworkFormTextInput from '@/modules/network/components/form/new/inputs/NetworkFormTextInput.vue'
|
||||
import NewNetworkDescriptionTextarea from '@/modules/network/components/form/new/inputs/NewNetworkDescriptionTextarea.vue'
|
||||
@@ -27,6 +26,7 @@ import NewNetworkButtonsSection from '@/modules/network/components/form/new/NewN
|
||||
import { useNewBondedNetworkForm } from '@/modules/network/form/new-bonded/use-new-bonded-network-form.ts'
|
||||
import type { NewBondedNetworkPayload } from '@/modules/network/jobs/bonded-network-create.job.ts'
|
||||
import VtsForm from '@core/components/form/VtsForm.vue'
|
||||
import VtsNumberInput from '@core/components/number-input/VtsNumberInput.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<NetworkFormTextInput v-bind="nameInputBindings" />
|
||||
<NetworkFormNumberInput v-bind="mtuInputBindings" />
|
||||
<VtsNumberInput v-bind="mtuInputBindings" />
|
||||
</div>
|
||||
<NewNetworkDescriptionTextarea v-bind="descriptionInputBindings" />
|
||||
</div>
|
||||
@@ -16,13 +16,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import NetworkFormNumberInput from '@/modules/network/components/form/new/inputs/NetworkFormNumberInput.vue'
|
||||
import NetworkFormTextInput from '@/modules/network/components/form/new/inputs/NetworkFormTextInput.vue'
|
||||
import NewNetworkDescriptionTextarea from '@/modules/network/components/form/new/inputs/NewNetworkDescriptionTextarea.vue'
|
||||
import NewNetworkNbdRadio from '@/modules/network/components/form/new/inputs/NewNetworkNbdRadio.vue'
|
||||
import NewNetworkButtonsSection from '@/modules/network/components/form/new/NewNetworkButtonsSection.vue'
|
||||
import { useNewInternalNetworkForm } from '@/modules/network/form/new-internal/use-new-internal-network-form.ts'
|
||||
import type { NewInternalNetworkPayload } from '@/modules/network/jobs/internal-network-create.job.ts'
|
||||
import VtsNumberInput from '@core/components/number-input/VtsNumberInput.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
|
||||
@@ -7,18 +7,17 @@
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<NetworkFormTextInput v-bind="nameInputBindings" />
|
||||
<NetworkFormNumberInput v-bind="mtuInputBindings" />
|
||||
<VtsNumberInput v-bind="mtuInputBindings" />
|
||||
</div>
|
||||
<NewNetworkDescriptionTextarea v-bind="descriptionInputBindings" />
|
||||
</div>
|
||||
<NetworkFormNumberInput v-bind="vlanInputBindings" class="vlan" />
|
||||
<VtsNumberInput v-bind="vlanInputBindings" class="vlan" />
|
||||
<NewNetworkNbdRadio class="nbd" v-bind="nbdRadioBindings" />
|
||||
<NewNetworkButtonsSection :cancel-to :submit-label="t('action:create-network')" />
|
||||
</VtsForm>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import NetworkFormNumberInput from '@/modules/network/components/form/new/inputs/NetworkFormNumberInput.vue'
|
||||
import NetworkFormSelect from '@/modules/network/components/form/new/inputs/NetworkFormSelect.vue'
|
||||
import NetworkFormTextInput from '@/modules/network/components/form/new/inputs/NetworkFormTextInput.vue'
|
||||
import NewNetworkDescriptionTextarea from '@/modules/network/components/form/new/inputs/NewNetworkDescriptionTextarea.vue'
|
||||
@@ -27,6 +26,7 @@ import NewNetworkButtonsSection from '@/modules/network/components/form/new/NewN
|
||||
import { useNewNetworkForm } from '@/modules/network/form/new/use-new-network-form.ts'
|
||||
import type { NewNetworkPayload } from '@/modules/network/jobs/network-create.job.ts'
|
||||
import VtsForm from '@core/components/form/VtsForm.vue'
|
||||
import VtsNumberInput from '@core/components/number-input/VtsNumberInput.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<VtsInputWrapper :label :message="messages">
|
||||
<UiInput v-model.number="model" accent="brand" type="number" :required @blur="emit('blur')" />
|
||||
</VtsInputWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { InputWrapperMessage } from '@core/components/input-wrapper/VtsInputWrapper.vue'
|
||||
import VtsInputWrapper from '@core/components/input-wrapper/VtsInputWrapper.vue'
|
||||
import UiInput from '@core/components/ui/input/UiInput.vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { info, warning, error } = defineProps<{
|
||||
label: string
|
||||
error?: InputWrapperMessage
|
||||
warning?: InputWrapperMessage
|
||||
info?: string
|
||||
required?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ blur: [] }>()
|
||||
const model = defineModel<number | undefined>()
|
||||
|
||||
const messages = computed<InputWrapperMessage>(
|
||||
() => [info, warning, error].filter(message => message !== undefined) as InputWrapperMessage
|
||||
)
|
||||
</script>
|
||||
52
@xen-orchestra/lite/src/route-map.d.ts
vendored
52
@xen-orchestra/lite/src/route-map.d.ts
vendored
@@ -422,6 +422,13 @@ declare module 'vue-router/auto-routes' {
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/story/web-core/number-input/vts-number-input': RouteRecordInfo<
|
||||
'/story/web-core/number-input/vts-number-input',
|
||||
'/story/web-core/number-input/vts-number-input',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/story/web-core/object-icon/vts-object-icon': RouteRecordInfo<
|
||||
'/story/web-core/object-icon/vts-object-icon',
|
||||
'/story/web-core/object-icon/vts-object-icon',
|
||||
@@ -499,6 +506,13 @@ declare module 'vue-router/auto-routes' {
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/story/web-core/success-state-hero/vts-success-state-hero': RouteRecordInfo<
|
||||
'/story/web-core/success-state-hero/vts-success-state-hero',
|
||||
'/story/web-core/success-state-hero/vts-success-state-hero',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/story/web-core/tab/router-tab': RouteRecordInfo<
|
||||
'/story/web-core/tab/router-tab',
|
||||
'/story/web-core/tab/router-tab',
|
||||
@@ -527,6 +541,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',
|
||||
@@ -828,6 +849,13 @@ declare module 'vue-router/auto-routes' {
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/story/web-core/ui/numbered-stepper/ui-numbered-stepper': RouteRecordInfo<
|
||||
'/story/web-core/ui/numbered-stepper/ui-numbered-stepper',
|
||||
'/story/web-core/ui/numbered-stepper/ui-numbered-stepper',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/story/web-core/ui/object-link/ui-object-link': RouteRecordInfo<
|
||||
'/story/web-core/ui/object-link/ui-object-link',
|
||||
'/story/web-core/ui/object-link/ui-object-link',
|
||||
@@ -1475,6 +1503,12 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/stories/web-core/number-input/vts-number-input.story.vue': {
|
||||
routes:
|
||||
| '/story/web-core/number-input/vts-number-input'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/stories/web-core/object-icon/vts-object-icon.story.vue': {
|
||||
routes:
|
||||
| '/story/web-core/object-icon/vts-object-icon'
|
||||
@@ -1541,6 +1575,12 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/stories/web-core/success-state-hero/vts-success-state-hero.story.vue': {
|
||||
routes:
|
||||
| '/story/web-core/success-state-hero/vts-success-state-hero'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/stories/web-core/tab/router-tab.story.vue': {
|
||||
routes:
|
||||
| '/story/web-core/tab/router-tab'
|
||||
@@ -1565,6 +1605,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'
|
||||
@@ -1823,6 +1869,12 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/stories/web-core/ui/numbered-stepper/ui-numbered-stepper.story.vue': {
|
||||
routes:
|
||||
| '/story/web-core/ui/numbered-stepper/ui-numbered-stepper'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/stories/web-core/ui/object-link/ui-object-link.story.vue': {
|
||||
routes:
|
||||
| '/story/web-core/ui/object-link/ui-object-link'
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Usage
|
||||
|
||||
A number field with its label and its info, warning and error messages, ready for the field bindings of a validated
|
||||
form. A cleared input gives an empty value, not `0`.
|
||||
|
||||
```vue-template
|
||||
<VtsNumberInput v-model="quantity" label="Quantity" info="Between 1 and 8" />
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
model().type('number | undefined').preset(2),
|
||||
prop('label').str().required().preset('Quantity').widget(),
|
||||
prop('info').str().widget(),
|
||||
prop('warning').type('InputWrapperMessage').widget(object()),
|
||||
prop('error').type('InputWrapperMessage').widget(object()),
|
||||
prop('required').bool().widget(),
|
||||
event('blur'),
|
||||
]"
|
||||
>
|
||||
<VtsNumberInput v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { event, model, prop } from '@/libs/story/story-param.ts'
|
||||
import { object } from '@/libs/story/story-widget.ts'
|
||||
import VtsNumberInput from '@core/components/number-input/VtsNumberInput.vue'
|
||||
</script>
|
||||
@@ -0,0 +1,13 @@
|
||||
# Usage
|
||||
|
||||
The card telling an action succeeded: an optional title, the success alert, then the actions that follow, centred.
|
||||
|
||||
```vue-template
|
||||
<VtsSuccessStateHero>
|
||||
<template #title>Connected to 192.168.1.1</template>
|
||||
The pool is connected.
|
||||
<template #actions>
|
||||
<UiLink :to="poolRoute" size="medium">Visit the pool dashboard</UiLink>
|
||||
</template>
|
||||
</VtsSuccessStateHero>
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ settings }"
|
||||
:params="[
|
||||
slot('title').help('Optional, shown above the alert'),
|
||||
slot().help('The text of the success alert'),
|
||||
slot('actions').help('The links and buttons that follow the success, centred'),
|
||||
setting('titleSlot').preset('Connected to 192.168.1.1').widget(),
|
||||
setting('defaultSlot').preset('The pool is connected.').widget(),
|
||||
]"
|
||||
>
|
||||
<VtsSuccessStateHero>
|
||||
<template v-if="settings.titleSlot" #title>{{ settings.titleSlot }}</template>
|
||||
{{ settings.defaultSlot }}
|
||||
<template #actions>
|
||||
<UiButton accent="brand" variant="secondary" size="medium">Continue</UiButton>
|
||||
</template>
|
||||
</VtsSuccessStateHero>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { setting, slot } from '@/libs/story/story-param.ts'
|
||||
import VtsSuccessStateHero from '@core/components/success-state-hero/VtsSuccessStateHero.vue'
|
||||
import UiButton from '@core/components/ui/button/UiButton.vue'
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
prop('steps').arr('NumberedStepDefinition').required().preset(steps),
|
||||
prop('currentStep').num().required().preset(0).help('Index of the current step, earlier steps are done'),
|
||||
]"
|
||||
:presets
|
||||
>
|
||||
<UiNumberedStepper v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { prop } from '@/libs/story/story-param.ts'
|
||||
import UiNumberedStepper, {
|
||||
type NumberedStepDefinition,
|
||||
} from '@core/components/ui/numbered-stepper/UiNumberedStepper.vue'
|
||||
|
||||
const steps: NumberedStepDefinition[] = [
|
||||
{ label: 'General information' },
|
||||
{ label: 'Settings' },
|
||||
{ label: 'Options' },
|
||||
{ label: 'Review' },
|
||||
]
|
||||
|
||||
const presets = {
|
||||
'First step': { props: { steps, currentStep: 0 } },
|
||||
'Intermediate step': { props: { steps, currentStep: 2 } },
|
||||
'Last step': { props: { steps, currentStep: 3 } },
|
||||
}
|
||||
</script>
|
||||
@@ -211,3 +211,14 @@ So, a small utility can be used to add this style on the element that needs it:
|
||||
```
|
||||
|
||||
Then, you can tweak the `inset` property to get the desired result.
|
||||
|
||||
## `•` separator utility
|
||||
|
||||
To separate inline parts with an interpunct (`•`), put the global `interpunct-list` class on their container: each rendered child after the first gets the separator, so a child hidden by `v-if` leaves no stray one.
|
||||
|
||||
```html
|
||||
<span class="interpunct-list">
|
||||
<span>{{ t('n-hosts', { n: hostCount }) }}</span>
|
||||
<span v-if="srCount !== undefined">{{ t('n-srs', { n: srCount }) }}</span>
|
||||
</span>
|
||||
```
|
||||
|
||||
@@ -95,3 +95,9 @@ pre {
|
||||
content: '\2014'; /* em dash — */
|
||||
margin-inline-end: 0.8rem;
|
||||
}
|
||||
|
||||
/* Between rendered children only: a child hidden by `v-if` leaves no stray separator. Empty alt text: not read out */
|
||||
.interpunct-list > * + *::before {
|
||||
content: '•' / '';
|
||||
margin-inline: 0.8rem;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import type { InputWrapperMessage } from '@core/components/input-wrapper/VtsInputWrapper.vue'
|
||||
import VtsInputWrapper from '@core/components/input-wrapper/VtsInputWrapper.vue'
|
||||
import UiInput from '@core/components/ui/input/UiInput.vue'
|
||||
import { mergeInputWrapperMessages } from '@core/utils/input-wrapper-message.util.ts'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { info, warning, error } = defineProps<{
|
||||
@@ -19,9 +20,8 @@ const { info, warning, error } = defineProps<{
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ blur: [] }>()
|
||||
|
||||
const model = defineModel<number | undefined>()
|
||||
|
||||
const messages = computed<InputWrapperMessage>(
|
||||
() => [info, warning, error].filter(message => message !== undefined) as InputWrapperMessage
|
||||
)
|
||||
const messages = computed(() => mergeInputWrapperMessages(info, warning, error))
|
||||
</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,69 @@
|
||||
<template>
|
||||
<VtsStateHero class="vts-success-state-hero" type="all-good" format="card" size="medium">
|
||||
<div class="container">
|
||||
<div v-if="slots.title" class="typo-h1 title"><slot name="title" /></div>
|
||||
<div class="content" :class="{ mobile: uiStore.isSmall, desktop: !uiStore.isLarge }">
|
||||
<UiAlert accent="success"><slot /></UiAlert>
|
||||
<div class="action-buttons" :class="{ mobile: uiStore.isSmall }">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VtsStateHero>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import UiAlert from '@core/components/ui/alert/UiAlert.vue'
|
||||
import { useUiStore } from '@core/stores/ui.store.ts'
|
||||
|
||||
const slots = defineSlots<{
|
||||
title?(): any
|
||||
default(): any
|
||||
actions(): any
|
||||
}>()
|
||||
|
||||
const uiStore = useUiStore()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.vts-success-state-hero {
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--color-neutral-txt-primary);
|
||||
}
|
||||
|
||||
.content {
|
||||
color: var(--color-neutral-txt-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rem;
|
||||
margin: 0 auto;
|
||||
width: 65%;
|
||||
|
||||
&.desktop {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2.4rem;
|
||||
|
||||
&.mobile {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,105 @@
|
||||
<!-- v1 -->
|
||||
<template>
|
||||
<ol class="ui-numbered-stepper">
|
||||
<li
|
||||
v-for="step in stepItems"
|
||||
:key="step.number"
|
||||
class="step"
|
||||
:class="{ done: step.isDone, current: step.isCurrent }"
|
||||
:aria-current="step.isCurrent ? 'step' : undefined"
|
||||
>
|
||||
<span class="marker typo-caption-small" aria-hidden="true">
|
||||
<VtsIcon v-if="step.isDone" name="fa:check" size="small" />
|
||||
<template v-else>{{ step.number }}</template>
|
||||
</span>
|
||||
<span class="label" :class="step.isCurrent ? 'typo-body-bold-small' : 'typo-body-regular-small'">
|
||||
{{ step.label }}
|
||||
<span v-if="step.isDone" class="visually-hidden">{{ t('done') }}</span>
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export type NumberedStepDefinition = {
|
||||
label: string
|
||||
}
|
||||
|
||||
const { steps, currentStep } = defineProps<{
|
||||
steps: NumberedStepDefinition[]
|
||||
currentStep: number
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const stepItems = computed(() =>
|
||||
steps.map((step, index) => ({
|
||||
label: step.label,
|
||||
number: index + 1,
|
||||
isDone: index < currentStep,
|
||||
isCurrent: index === currentStep,
|
||||
}))
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-numbered-stepper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
|
||||
&:not(:last-child)::after {
|
||||
content: '';
|
||||
width: 4rem;
|
||||
height: 0.2rem;
|
||||
margin-inline: 0.4rem;
|
||||
background-color: var(--color-neutral-border);
|
||||
}
|
||||
|
||||
&:is(.done, .current) .label {
|
||||
color: var(--color-neutral-txt-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.marker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.6rem;
|
||||
height: 2.6rem;
|
||||
border: 0.1rem solid var(--color-neutral-border);
|
||||
border-radius: 50%;
|
||||
color: var(--color-neutral-txt-secondary);
|
||||
background-color: var(--color-neutral-background-primary);
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--color-neutral-txt-secondary);
|
||||
}
|
||||
|
||||
.done .marker {
|
||||
border-color: var(--color-success-item-base);
|
||||
color: var(--color-success-txt-base);
|
||||
background-color: var(--color-success-background-selected);
|
||||
}
|
||||
|
||||
.current .marker {
|
||||
border-color: var(--color-brand-item-base);
|
||||
color: var(--color-brand-txt-item);
|
||||
background-color: var(--color-brand-item-base);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -57,6 +57,19 @@ Now if the URL contains `?count=42`, then `count.value` will be the number `42`.
|
||||
|
||||
Setting `count.value = 0` will remove the query parameter from the URL.
|
||||
|
||||
A string subtype, such as a branded id, keeps its own type:
|
||||
|
||||
```typescript
|
||||
type ItemId = string & { readonly __brand: 'item' }
|
||||
|
||||
const itemId = useRouteQuery<ItemId | undefined>('item', {
|
||||
toData: query => (query === '' ? undefined : (query as ItemId)),
|
||||
toQuery: data => data ?? '',
|
||||
})
|
||||
```
|
||||
|
||||
Here `itemId.value` is an `ItemId | undefined`, and setting it to `undefined` removes the query parameter from the URL.
|
||||
|
||||
### Working with objects, arrays, Set, Map, and boolean
|
||||
|
||||
The `useRouteQuery` composable can handle any data type,
|
||||
|
||||
@@ -40,13 +40,16 @@ export type GuessActions<TData> = TData extends string | number
|
||||
? MapActions<TKey, TValue>
|
||||
: EmptyObject
|
||||
|
||||
// A string subtype (e.g. a branded id) is also an object type: it is matched first to keep it as is
|
||||
export type RouteQuery<TData> = WritableComputedRef<
|
||||
TData extends Set<infer V>
|
||||
? ReadonlySet<V>
|
||||
: TData extends Map<infer K, infer V>
|
||||
? ReadonlyMap<K, V>
|
||||
: TData extends object
|
||||
? Readonly<TData>
|
||||
: TData
|
||||
TData extends string
|
||||
? TData
|
||||
: TData extends Set<infer V>
|
||||
? ReadonlySet<V>
|
||||
: TData extends Map<infer K, infer V>
|
||||
? ReadonlyMap<K, V>
|
||||
: TData extends object
|
||||
? Readonly<TData>
|
||||
: TData
|
||||
> &
|
||||
GuessActions<TData>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -21,8 +21,11 @@
|
||||
"action:attach": "Attach",
|
||||
"action:attach-vdi": "@:attach-vdi",
|
||||
"action:back": "Back",
|
||||
"action:back-to-vms": "Back to VMs",
|
||||
"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",
|
||||
@@ -36,6 +39,7 @@
|
||||
"action:connect-n-vifs": "Connect 1 VIF | Connect {n} VIFs",
|
||||
"action:connect-pool": "Connect pool",
|
||||
"action:continue": "Continue",
|
||||
"action:continue-with-existing-list": "Continue with existing list",
|
||||
"action:copy": "Copy",
|
||||
"action:copy-all": "Copy all",
|
||||
"action:copy-host": "Copy host",
|
||||
@@ -85,7 +89,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",
|
||||
@@ -110,6 +116,7 @@
|
||||
"action:forget-host": "Forget host",
|
||||
"action:forget-n-brs": "@:action:forget 1 BR | @:action:forget {n} BRs",
|
||||
"action:forget-n-pifs": "Forget 1 PIF | Forget {n} PIFs",
|
||||
"action:forget-password": "Forget password",
|
||||
"action:go-back": "Go back",
|
||||
"action:group": "Group",
|
||||
"action:hide": "Hide",
|
||||
@@ -117,6 +124,7 @@
|
||||
"action:import-export": "Import/Export",
|
||||
"action:install-app": "Install {name}",
|
||||
"action:install-guest-tools": "Install guest tools",
|
||||
"action:launch-n-vms": "Launch 1 VM | Launch {n} VMs",
|
||||
"action:load": "Load",
|
||||
"action:load-now": "Load now",
|
||||
"action:log-out": "Log out",
|
||||
@@ -124,6 +132,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",
|
||||
@@ -135,6 +145,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",
|
||||
@@ -148,12 +161,15 @@
|
||||
"action:see-all": "See all",
|
||||
"action:see-less": "See less",
|
||||
"action:see-n-more": "See {n} more",
|
||||
"action:see-task": "See the task",
|
||||
"action:select-all": "Select all",
|
||||
"action:select-all-n-objects": "select {n} object | select all {n} objects",
|
||||
"action:select-n-objects-of": "{from} - {to} of {total} objects",
|
||||
"action:select-network": "Select a network",
|
||||
"action:select-none": "Select none",
|
||||
"action:select-pool": "Select a pool",
|
||||
"action:select-storage": "Select a storage",
|
||||
"action:select-template": "Select a template",
|
||||
"action:send": "Send",
|
||||
"action:send-ctrl-alt-del": "Send Ctrl+Alt+Del",
|
||||
"action:set-pif-management": "Set management PIF",
|
||||
@@ -182,6 +198,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",
|
||||
@@ -205,6 +223,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",
|
||||
@@ -213,6 +232,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",
|
||||
@@ -299,6 +319,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",
|
||||
@@ -370,11 +391,18 @@
|
||||
"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",
|
||||
"default-network": "Default network",
|
||||
"default-options": "Default options",
|
||||
"default-storage-repository": "Default storage repository",
|
||||
"default-template": "Default template",
|
||||
"default-value": "Default ({value})",
|
||||
"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…",
|
||||
@@ -385,10 +413,14 @@
|
||||
"deploy-xoa-status:waiting": "Waiting for XOA to respond…",
|
||||
"descending": "descending",
|
||||
"description": "Description",
|
||||
"destination": "Destination",
|
||||
"destination-host": "Destination host",
|
||||
"destination-network": "Destination network",
|
||||
"destination-network-required": "Destination network is required.",
|
||||
"destination-pool": "Destination pool",
|
||||
"destination-pool-named": "Destination pool {name}",
|
||||
"destination-sr": "Destination SR",
|
||||
"destination-vm": "Destination VM",
|
||||
"destination-sr-required": "Destination SR is required.",
|
||||
"details": "Details",
|
||||
"device": "Device",
|
||||
@@ -408,8 +440,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?",
|
||||
@@ -483,10 +517,13 @@
|
||||
"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",
|
||||
"graphics-display": "Graphics & Display",
|
||||
@@ -513,6 +550,7 @@
|
||||
"host-not-running": "Host is not running",
|
||||
"host-off": "Host is powered off",
|
||||
"host-or-ip-address": "Host or IP address",
|
||||
"host-required": "Host is required.",
|
||||
"host:status:active": "@:status:active",
|
||||
"host:status:disabled": "@:status:disabled",
|
||||
"host:status:halted": "@:status:halted",
|
||||
@@ -524,8 +562,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",
|
||||
@@ -687,6 +727,8 @@
|
||||
"job:sr-delete:missing-sr": "No SRs to delete",
|
||||
"job:sr-disconnect:missing-sr": "No SRs to disconnect",
|
||||
"job:traffic-rule-delete:missing-rule": "No traffic rule selected",
|
||||
"job:v2v-start:in-progress": "Migration launch in progress…",
|
||||
"job:v2v-start:missing-vm": "No VMs to migrate",
|
||||
"job:vbd-connect:missing-vbd": "No VBDs to connect",
|
||||
"job:vbd-connect:vbd-already-connected": "VBD is already connected",
|
||||
"job:vbd-delete:missing-vbd": "No VBDs to delete",
|
||||
@@ -769,10 +811,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",
|
||||
@@ -783,6 +827,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",
|
||||
@@ -797,6 +842,7 @@
|
||||
"loaded": "Loaded",
|
||||
"loading": "Loading",
|
||||
"local": "Local",
|
||||
"location": "Location",
|
||||
"locked": "Locked",
|
||||
"locking-mode": "Locking mode",
|
||||
"locking-mode-default": "Default locking mode",
|
||||
@@ -815,6 +861,7 @@
|
||||
"management-ip": "Management IP",
|
||||
"manual": "Manual",
|
||||
"manufacturer-info": "Manufacturer info",
|
||||
"mapping": "Mapping",
|
||||
"master": "Primary host",
|
||||
"maximum-dynamic-memory": "Maximum dynamic memory",
|
||||
"maximum-static-memory": "Maximum static memory",
|
||||
@@ -823,6 +870,8 @@
|
||||
"memory-usage": "Memory usage",
|
||||
"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",
|
||||
@@ -883,19 +932,28 @@
|
||||
"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-need-confirmation": "1 needs confirmation | {n} need confirmation",
|
||||
"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",
|
||||
@@ -905,6 +963,8 @@
|
||||
"n-vdis": "1 VDI | {n} VDIs",
|
||||
"n-vifs": "1 VIF | {n} VIFs",
|
||||
"n-vms": "1 VM | {n} VMs",
|
||||
"n-vms-in-list": "1 VM in the list | {n} VMs in the list",
|
||||
"n-warnings": "1 warning | {n} warnings",
|
||||
"name": "Name",
|
||||
"name-required": "Name is required.",
|
||||
"native": "Native",
|
||||
@@ -915,6 +975,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}",
|
||||
@@ -992,6 +1053,7 @@
|
||||
"no-pif-detected": "No PIFs detected",
|
||||
"no-plugin-detected": "No plugins detected",
|
||||
"no-pools-detected": "No pools detected",
|
||||
"no-remembered-connection": "No remembered connection",
|
||||
"no-replicated-vm": "This VM isn’t being replicated. Configure a replication job to enable it.",
|
||||
"no-result": "No results",
|
||||
"no-role-applied": "No role applied",
|
||||
@@ -1015,8 +1077,11 @@
|
||||
"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-supported-yet": "Not supported yet",
|
||||
"not-yet-available": "Information is not yet available",
|
||||
"object": "Object",
|
||||
"object-not-found": "Object {id} can't be found…",
|
||||
@@ -1048,6 +1113,8 @@
|
||||
"operation:vm-clean-shutdown": "VMs clean shutting down",
|
||||
"operation:vm-hard-shutdown": "VMs hard shutting down",
|
||||
"operation:vm-suspend": "VMs suspending",
|
||||
"option-off": "@:off",
|
||||
"option-on": "@:on",
|
||||
"optional": "Optional",
|
||||
"options": "Options",
|
||||
"or": "Or",
|
||||
@@ -1056,6 +1123,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",
|
||||
@@ -1075,6 +1143,7 @@
|
||||
"pause-blocked": "Pause Blocked",
|
||||
"pbd-details": "Physical Block Device (PBD) details",
|
||||
"pending": "Pending",
|
||||
"per-vm-options": "Per-VM options",
|
||||
"physical-interface-status": "Physical interface status",
|
||||
"pif": "PIF",
|
||||
"pif-status": "PIF status",
|
||||
@@ -1107,7 +1176,9 @@
|
||||
"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",
|
||||
"pool:status:disconnected": "@:status:disconnected",
|
||||
"pool:status:unreachable": "@:status:unreachable",
|
||||
@@ -1121,12 +1192,15 @@
|
||||
"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}",
|
||||
"preview": "Preview",
|
||||
"pro-support": "{name} pro support",
|
||||
"professional-support": "Professional support",
|
||||
"progress": "Progress",
|
||||
@@ -1180,6 +1254,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",
|
||||
@@ -1193,6 +1268,9 @@
|
||||
"relative-time:past": "{str} ago",
|
||||
"relative-time:second": "1 second | {n} seconds",
|
||||
"relative-time:year": "1 year | {n} years",
|
||||
"remember-connection-in-browser": "Remember connection in this browser",
|
||||
"remember-credentials-warning": "Storing these credentials in your browser increases security risks. Make sure this complies with your security guidelines.",
|
||||
"remembered-connections": "Remembered connections",
|
||||
"remote-syslog": "Remote syslog",
|
||||
"report-recipients": "Report recipients",
|
||||
"report-when": "Report when",
|
||||
@@ -1205,8 +1283,11 @@
|
||||
"resources": "Resources",
|
||||
"resources-overview": "Resources overview",
|
||||
"rest-api": "REST API",
|
||||
"resume": "Resume",
|
||||
"retry": "Retry",
|
||||
"review": "Review",
|
||||
"review-and-confirm": "Review & confirm",
|
||||
"review-and-launch": "Review and launch",
|
||||
"roles": "Roles",
|
||||
"roles-applied": "Roles applied",
|
||||
"root-by-default": "\"root\" by default.",
|
||||
@@ -1230,7 +1311,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",
|
||||
@@ -1241,6 +1324,9 @@
|
||||
"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",
|
||||
"smart-mode": "Smart mode",
|
||||
"smb": "SMB",
|
||||
@@ -1259,6 +1345,7 @@
|
||||
"software": "Software",
|
||||
"software-tooling": "Software & tooling",
|
||||
"source": "Source",
|
||||
"source-to-destination": "{source} → {destination}",
|
||||
"space": "Space",
|
||||
"speed": "Speed",
|
||||
"speed-limit": "Speed limit",
|
||||
@@ -1283,6 +1370,7 @@
|
||||
"ssh-login": "SSH login",
|
||||
"ssh-password": "SSH password",
|
||||
"ssh-password-confirm": "Confirm SSH password",
|
||||
"ssl-check-skipped": "SSL check skipped",
|
||||
"stacked-cpu-usage": "Stacked CPU usage",
|
||||
"stacked-ram-usage": "Stacked RAM usage",
|
||||
"start-console": "Start your {type} to access the console and resume operations!",
|
||||
@@ -1309,6 +1397,7 @@
|
||||
"status:unknown": "Unknown",
|
||||
"status:unreachable": "Unreachable",
|
||||
"step-n-of": "Step {current} / {total}",
|
||||
"stop-on-error": "Stop on error",
|
||||
"storage": "Storage",
|
||||
"storage-configuration": "Storage configuration",
|
||||
"storage-format": "Storage format",
|
||||
@@ -1316,10 +1405,12 @@
|
||||
"storage-repositories": "Storage repositories",
|
||||
"storage-repository": "Storage repository",
|
||||
"storage-usage": "Storage usage",
|
||||
"stored-in-browser-only": "Stored in this browser only",
|
||||
"subfolder": "Subfolder",
|
||||
"success": "Success",
|
||||
"summary": "Summary",
|
||||
"support": "Support",
|
||||
"supported": "Supported",
|
||||
"supported-image-formats": "Supported image formats",
|
||||
"suspend-blocked": "Suspend Blocked",
|
||||
"suspend-storage-repository": "Suspend storage repository",
|
||||
@@ -1335,6 +1426,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",
|
||||
@@ -1347,6 +1440,7 @@
|
||||
"tasks:overview": "Tasks overview",
|
||||
"template": "Template",
|
||||
"template-has-bios-strings": "The template already contains the BIOS strings",
|
||||
"template-required": "Template is required.",
|
||||
"test-data": "Test data",
|
||||
"theme": "Theme",
|
||||
"theme-auto": "Auto",
|
||||
@@ -1422,6 +1516,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",
|
||||
@@ -1429,6 +1524,7 @@
|
||||
"use-https": "Use HTTPS",
|
||||
"used": "Used",
|
||||
"used-for-backup": "Used for backup",
|
||||
"used-relative-time": "used {time}",
|
||||
"used-space": "Used space",
|
||||
"used-space-on-sr": "Used space on SR",
|
||||
"user": "User",
|
||||
@@ -1438,7 +1534,72 @@
|
||||
"username-required": "Username is required.",
|
||||
"users": "Users",
|
||||
"uuid": "UUID",
|
||||
"v2v": "V2V",
|
||||
"v2v:action:add-to-migration-list": "Add to migration list",
|
||||
"v2v:already-fully-migrated": "Already fully migrated",
|
||||
"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:clean-up-preview": "Clean up (preview)",
|
||||
"v2v:clean-up-vm": "Automatically clean up the VM",
|
||||
"v2v:connect-new-vmware-source": "Connect a new VMware source",
|
||||
"v2v:continues-on-pool-sr": "continues on pool {pool}, SR {srs}",
|
||||
"v2v:create-second-copy": "Create a second copy of {name}",
|
||||
"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:cutover-done-on": "Cutover done on {date}.",
|
||||
"v2v:default-options-info": "Apply to every VM unless overridden below",
|
||||
"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:launch-group:excluded": "Excluded",
|
||||
"v2v:launch-group:new": "New",
|
||||
"v2v:launch-group:resumed": "Resumed",
|
||||
"v2v:launch-problems": "The migration cannot be started",
|
||||
"v2v:launch-success": "The migration is running: its task tells its progress.",
|
||||
"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:migrated-vm-details": "Existing VM: {vm} on pool {pool}, SR {srs}. A second VM will be created on pool {destinationPool}.",
|
||||
"v2v:n-excluded": "1 excluded | {n} excluded",
|
||||
"v2v:n-vms-launched": "1 VM launched | {n} VMs launched",
|
||||
"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:per-vm-options-info": "\"Default\" follows the default options above",
|
||||
"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:start-on-xcp-ng": "Start on XCP-ng",
|
||||
"v2v:start-vm-on-xcp-ng": "Start the VM on XCP-ng",
|
||||
"v2v:status:cold": "Cutover done",
|
||||
"v2v:status:warm": "Warm synced",
|
||||
"v2v:stop-on-error-info": "Stop on the first error when importing VMs. Applies to the whole launch.",
|
||||
"v2v:stop-source": "Stop source",
|
||||
"v2v:stop-source-vm": "Stop the source VM",
|
||||
"v2v:stop-source-vm-info": "Source VM stopped before the last delta transfer (after final snapshot). Needed to fully transfer a running VM.",
|
||||
"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:tags-mac-not-applied": "Tags and the Keep MAC addresses option of the mapping step are not applied yet.",
|
||||
"v2v:unsupported-encrypted-disk": "Unsupported: encrypted disk",
|
||||
"v2v:unsupported-independent-disk": "Unsupported: independent disk",
|
||||
"v2v:unsupported-rdm-disk": "Unsupported: RDM disk",
|
||||
"v2v:vm-start-problem": "{vm}: {message}",
|
||||
"value-by-default": "{value} by default",
|
||||
"value-from": "Value from",
|
||||
"vcpu": "vCPU",
|
||||
"vcpu-at-startup": "vCPU at startup",
|
||||
"vcpus": "vCPUs",
|
||||
@@ -1487,6 +1648,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",
|
||||
@@ -1527,6 +1689,7 @@
|
||||
"vms-status": "VMs status",
|
||||
"vms-tags": "VMs tags",
|
||||
"vms-treeview": "VMs treeview",
|
||||
"vmware": "VMware",
|
||||
"vtpm": "VTPM",
|
||||
"warning": "Warning",
|
||||
"warning:vdi-already-attached": "The selected VDI is already attached to this VM.",
|
||||
|
||||
@@ -21,8 +21,11 @@
|
||||
"action:attach": "Attacher",
|
||||
"action:attach-vdi": "@:attach-vdi",
|
||||
"action:back": "Retour",
|
||||
"action:back-to-vms": "Retour aux VMs",
|
||||
"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",
|
||||
@@ -36,6 +39,7 @@
|
||||
"action:connect-n-vifs": "Connecter 1 VIF | Connecter {n} VIFs",
|
||||
"action:connect-pool": "Connecter un pool",
|
||||
"action:continue": "Continuer",
|
||||
"action:continue-with-existing-list": "Continuer avec la liste existante",
|
||||
"action:copy": "Copier",
|
||||
"action:copy-all": "Copier tout",
|
||||
"action:copy-host": "Copier l'hôte",
|
||||
@@ -85,7 +89,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",
|
||||
@@ -110,6 +116,7 @@
|
||||
"action:forget-host": "Oublier l'hôte",
|
||||
"action:forget-n-brs": "@:action:forget 1 BR | @:action:forget {n} BRs",
|
||||
"action:forget-n-pifs": "Oublier 1 PIF | Oublier {n} PIFs",
|
||||
"action:forget-password": "Oublier le mot de passe",
|
||||
"action:go-back": "Revenir en arrière",
|
||||
"action:group": "Grouper",
|
||||
"action:hide": "Masquer",
|
||||
@@ -117,6 +124,7 @@
|
||||
"action:import-export": "Importer/Exporter",
|
||||
"action:install-app": "Installer {name}",
|
||||
"action:install-guest-tools": "Installer les guest tools",
|
||||
"action:launch-n-vms": "Lancer 0 VM | Lancer 1 VM | Lancer {n} VMs",
|
||||
"action:load": "Charger",
|
||||
"action:load-now": "Charger maintenant",
|
||||
"action:log-out": "Se déconnecter",
|
||||
@@ -124,6 +132,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",
|
||||
@@ -135,6 +145,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",
|
||||
@@ -148,12 +161,15 @@
|
||||
"action:see-all": "Voir tout",
|
||||
"action:see-less": "Voir moins",
|
||||
"action:see-n-more": "Voir {n} de plus",
|
||||
"action:see-task": "Voir la tâche",
|
||||
"action:select-all": "Tout sélectionner",
|
||||
"action:select-all-n-objects": "sélectionner {n} objet | sélectionner les {n} objets",
|
||||
"action:select-n-objects-of": "{from} - {to} de {total} objets",
|
||||
"action:select-network": "Sélectionner un réseau",
|
||||
"action:select-none": "Tout désélectionner",
|
||||
"action:select-pool": "Sélectionner un pool",
|
||||
"action:select-storage": "Sélectionner un SR",
|
||||
"action:select-template": "Sélectionner un modèle",
|
||||
"action:send": "Envoyer",
|
||||
"action:send-ctrl-alt-del": "Envoyer Ctrl+Alt+Suppr",
|
||||
"action:set-pif-management": "Définir comme PIF de gestion",
|
||||
@@ -182,6 +198,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",
|
||||
@@ -205,6 +223,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é",
|
||||
@@ -213,6 +232,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",
|
||||
@@ -299,6 +319,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",
|
||||
@@ -370,11 +391,18 @@
|
||||
"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",
|
||||
"default-network": "Réseau par défaut",
|
||||
"default-options": "Options par défaut",
|
||||
"default-storage-repository": "Dépôt de stockage par défaut",
|
||||
"default-template": "Modèle par défaut",
|
||||
"default-value": "Par défaut ({value})",
|
||||
"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…",
|
||||
@@ -385,10 +413,14 @@
|
||||
"deploy-xoa-status:waiting": "En attente de réponse de la XOA…",
|
||||
"descending": "descendant",
|
||||
"description": "Description",
|
||||
"destination": "Destination",
|
||||
"destination-host": "Hôte de destination",
|
||||
"destination-network": "Réseau de destination",
|
||||
"destination-network-required": "Le réseau de destination est requis.",
|
||||
"destination-pool": "Pool de destination",
|
||||
"destination-pool-named": "Pool de destination {name}",
|
||||
"destination-sr": "SR de destination",
|
||||
"destination-vm": "VM de destination",
|
||||
"destination-sr-required": "Le SR de destination est requis.",
|
||||
"details": "Détails",
|
||||
"device": "Appareil",
|
||||
@@ -408,8 +440,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 ?",
|
||||
@@ -483,10 +517,13 @@
|
||||
"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",
|
||||
"graphics-display": "Graphique & Affichage",
|
||||
@@ -513,6 +550,7 @@
|
||||
"host-not-running": "Hôte non démarré",
|
||||
"host-off": "L’hôte est éteint",
|
||||
"host-or-ip-address": "Hôte ou adresse IP",
|
||||
"host-required": "L'hôte est requis.",
|
||||
"host:status:active": "@:status:active",
|
||||
"host:status:disabled": "@:status:disabled",
|
||||
"host:status:halted": "@:status:halted",
|
||||
@@ -524,8 +562,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",
|
||||
@@ -687,6 +727,8 @@
|
||||
"job:sr-delete:missing-sr": "Aucun SR à supprimer",
|
||||
"job:sr-disconnect:missing-sr": "Aucun SR à déconnecter",
|
||||
"job:traffic-rule-delete:missing-rule": "Aucune règle de trafic sélectionnée",
|
||||
"job:v2v-start:in-progress": "Lancement de la migration en cours…",
|
||||
"job:v2v-start:missing-vm": "Aucune VM à migrer",
|
||||
"job:vbd-connect:missing-vbd": "Aucun VBD à connecter",
|
||||
"job:vbd-connect:vbd-already-connected": "Le VBD est déjà connecté",
|
||||
"job:vbd-delete:missing-vbd": "Aucun VBD à supprimer",
|
||||
@@ -769,10 +811,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",
|
||||
@@ -783,6 +827,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",
|
||||
@@ -797,6 +842,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",
|
||||
@@ -815,6 +861,7 @@
|
||||
"management-ip": "IP principale",
|
||||
"manual": "Manuel",
|
||||
"manufacturer-info": "Informations sur le fabricant",
|
||||
"mapping": "Correspondance",
|
||||
"master": "Hôte primaire",
|
||||
"maximum-dynamic-memory": "Mémoire dynamique maximale",
|
||||
"maximum-static-memory": "Mémoire statique maximale",
|
||||
@@ -823,6 +870,8 @@
|
||||
"memory-usage": "Utilisation de la mémoire",
|
||||
"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",
|
||||
@@ -883,19 +932,28 @@
|
||||
"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-need-confirmation": "0 à confirmer | 1 à confirmer | {n} à confirmer",
|
||||
"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",
|
||||
@@ -905,6 +963,8 @@
|
||||
"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-vms-in-list": "0 VM dans la liste | 1 VM dans la liste | {n} VMs dans la liste",
|
||||
"n-warnings": "0 avertissement | 1 avertissement | {n} avertissements",
|
||||
"name": "Nom",
|
||||
"name-required": "Le nom est requis.",
|
||||
"native": "Native",
|
||||
@@ -915,6 +975,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}",
|
||||
@@ -992,6 +1053,7 @@
|
||||
"no-pif-detected": "Aucun PIF détecté",
|
||||
"no-plugin-detected": "Aucun plugin détecté",
|
||||
"no-pools-detected": "Aucun pool détecté",
|
||||
"no-remembered-connection": "Aucune connexion mémorisée",
|
||||
"no-replicated-vm": "Cette VM n'est pas répliquée. Configurez une tâche de réplication pour l'activer.",
|
||||
"no-result": "Aucun résultat",
|
||||
"no-role-applied": "Pas de rôle appliqué",
|
||||
@@ -1015,8 +1077,11 @@
|
||||
"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-supported-yet": "Pas encore pris en charge",
|
||||
"not-yet-available": "Information pas encore disponible",
|
||||
"object": "Objet",
|
||||
"object-not-found": "L'objet {id} est introuvable…",
|
||||
@@ -1048,6 +1113,8 @@
|
||||
"operation:vm-clean-shutdown": "Arrêt des VMs",
|
||||
"operation:vm-hard-shutdown": "Arrêt forcé des VMs",
|
||||
"operation:vm-suspend": "Suspension des VMs",
|
||||
"option-off": "Désactivé",
|
||||
"option-on": "Activé",
|
||||
"optional": "Facultatif",
|
||||
"options": "Options",
|
||||
"or": "Ou",
|
||||
@@ -1056,6 +1123,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",
|
||||
@@ -1075,6 +1143,7 @@
|
||||
"pause-blocked": "Mise en pause bloquée",
|
||||
"pbd-details": "Détails du périphérique de bloc physique (PBD)",
|
||||
"pending": "En cours",
|
||||
"per-vm-options": "Options par VM",
|
||||
"physical-interface-status": "Statut de l'interface physique",
|
||||
"pif": "PIF",
|
||||
"pif-status": "Statut du PIF",
|
||||
@@ -1107,7 +1176,9 @@
|
||||
"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",
|
||||
"pool:status:disconnected": "@:status:disconnected",
|
||||
"pool:status:unreachable": "@:status:unreachable",
|
||||
@@ -1121,12 +1192,15 @@
|
||||
"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}",
|
||||
"preview": "Aperçu",
|
||||
"pro-support": "Support pro {name}",
|
||||
"professional-support": "Support professionnel",
|
||||
"progress": "Progression",
|
||||
@@ -1180,6 +1254,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",
|
||||
@@ -1193,6 +1268,9 @@
|
||||
"relative-time:past": "Il y a {str}",
|
||||
"relative-time:second": "1 seconde | {n} secondes",
|
||||
"relative-time:year": "1 an | {n} ans",
|
||||
"remember-connection-in-browser": "Mémoriser la connexion dans ce navigateur",
|
||||
"remember-credentials-warning": "Stocker ces identifiants dans votre navigateur augmente les risques de sécurité. Assurez-vous que cela respecte vos règles de sécurité.",
|
||||
"remembered-connections": "Connexions mémorisées",
|
||||
"remote-syslog": "Syslog distant",
|
||||
"report-recipients": "Destinataires du rapport",
|
||||
"report-when": "Rapporter quand",
|
||||
@@ -1205,8 +1283,11 @@
|
||||
"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",
|
||||
"review-and-launch": "Revue et lancement",
|
||||
"roles": "Rôles",
|
||||
"roles-applied": "Rôles appliqués",
|
||||
"root-by-default": "\"root\" par défaut.",
|
||||
@@ -1230,7 +1311,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",
|
||||
@@ -1241,6 +1324,9 @@
|
||||
"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é",
|
||||
"smart-mode": "Mode intelligent",
|
||||
"smb": "SMB",
|
||||
@@ -1259,6 +1345,7 @@
|
||||
"software": "Logiciel",
|
||||
"software-tooling": "Logiciels et outils",
|
||||
"source": "Source",
|
||||
"source-to-destination": "{source} → {destination}",
|
||||
"space": "Espace",
|
||||
"speed": "Vitesse",
|
||||
"speed-limit": "Limite de vitesse",
|
||||
@@ -1283,6 +1370,7 @@
|
||||
"ssh-login": "Nom d'utilisateur SSH",
|
||||
"ssh-password": "Mot de passe SSH",
|
||||
"ssh-password-confirm": "Confirmer le mot de passe SSH",
|
||||
"ssl-check-skipped": "Vérification SSL ignorée",
|
||||
"stacked-cpu-usage": "Utilisation CPU empilée",
|
||||
"stacked-ram-usage": "Utilisation RAM empilée",
|
||||
"start-console": "Démarrez votre {type} pour accéder à la console et reprendre les opérations !",
|
||||
@@ -1309,6 +1397,7 @@
|
||||
"status:unknown": "Inconnu",
|
||||
"status:unreachable": "Injoignable | Injoignable | Injoignables",
|
||||
"step-n-of": "Étape {current} / {total}",
|
||||
"stop-on-error": "Arrêter en cas d’erreur",
|
||||
"storage": "Stockage",
|
||||
"storage-configuration": "Configuration du stockage",
|
||||
"storage-format": "Format de stockage",
|
||||
@@ -1316,10 +1405,12 @@
|
||||
"storage-repositories": "Dépôts de stockage",
|
||||
"storage-repository": "Dépôt de stockage",
|
||||
"storage-usage": "Utilisation du stockage",
|
||||
"stored-in-browser-only": "Uniquement dans ce navigateur",
|
||||
"subfolder": "Sous-dossier",
|
||||
"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",
|
||||
@@ -1335,6 +1426,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",
|
||||
@@ -1347,6 +1440,7 @@
|
||||
"tasks:overview": "Vue d'ensemble des tâches",
|
||||
"template": "Modèle",
|
||||
"template-has-bios-strings": "Le modèle contient déjà les BIOS strings",
|
||||
"template-required": "Le modèle est requis.",
|
||||
"test-data": "Données de test",
|
||||
"theme": "Thème",
|
||||
"theme-auto": "Auto",
|
||||
@@ -1422,6 +1516,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",
|
||||
@@ -1429,6 +1524,7 @@
|
||||
"use-https": "Utiliser HTTPS",
|
||||
"used": "Utilisé",
|
||||
"used-for-backup": "Utilisé pour la sauvegarde",
|
||||
"used-relative-time": "dernière utilisation {time}",
|
||||
"used-space": "Espace utilisé",
|
||||
"used-space-on-sr": "Espace utilisé sur le SR",
|
||||
"user": "Utilisateur",
|
||||
@@ -1438,7 +1534,72 @@
|
||||
"username-required": "Le nom d'utilisateur est requis.",
|
||||
"users": "Utilisateurs",
|
||||
"uuid": "UUID",
|
||||
"v2v": "V2V",
|
||||
"v2v:action:add-to-migration-list": "Ajouter à la liste de migration",
|
||||
"v2v:already-fully-migrated": "Déjà entièrement migrées",
|
||||
"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:clean-up-preview": "Nettoyage (aperçu)",
|
||||
"v2v:clean-up-vm": "Nettoyer automatiquement la VM",
|
||||
"v2v:connect-new-vmware-source": "Connecter une nouvelle source VMware",
|
||||
"v2v:continues-on-pool-sr": "continue sur le pool {pool}, SR {srs}",
|
||||
"v2v:create-second-copy": "Créer une seconde copie de {name}",
|
||||
"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:cutover-done-on": "Bascule effectuée le {date}.",
|
||||
"v2v:default-options-info": "S’appliquent à chaque VM, sauf si elles sont remplacées ci-dessous",
|
||||
"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:launch-group:excluded": "Exclues",
|
||||
"v2v:launch-group:new": "Nouvelles",
|
||||
"v2v:launch-group:resumed": "Reprises",
|
||||
"v2v:launch-problems": "La migration ne peut pas être lancée",
|
||||
"v2v:launch-success": "La migration est en cours : sa tâche indique sa progression.",
|
||||
"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:migrated-vm-details": "VM existante : {vm} sur le pool {pool}, SR {srs}. Une seconde VM sera créée sur le pool {destinationPool}.",
|
||||
"v2v:n-excluded": "0 exclue | 1 exclue | {n} exclues",
|
||||
"v2v:n-vms-launched": "0 VM lancée | 1 VM lancée | {n} VMs lancées",
|
||||
"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:per-vm-options-info": "« Par défaut » suit les options par défaut ci-dessus",
|
||||
"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:start-on-xcp-ng": "Démarrer sur XCP-ng",
|
||||
"v2v:start-vm-on-xcp-ng": "Démarrer la VM sur XCP-ng",
|
||||
"v2v:status:cold": "Bascule effectuée",
|
||||
"v2v:status:warm": "Synchronisée à chaud",
|
||||
"v2v:stop-on-error-info": "S’arrêter à la première erreur lors de l’import des VMs. S’applique à tout le lancement.",
|
||||
"v2v:stop-source": "Arrêt de la source",
|
||||
"v2v:stop-source-vm": "Arrêter la VM source",
|
||||
"v2v:stop-source-vm-info": "VM source arrêtée avant le dernier transfert delta (après le snapshot final). Nécessaire pour transférer entièrement une VM en cours d’exécution.",
|
||||
"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:tags-mac-not-applied": "Les tags et l’option Conserver les adresses MAC de l’étape Correspondance ne sont pas encore appliqués.",
|
||||
"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",
|
||||
"v2v:vm-start-problem": "{vm} : {message}",
|
||||
"value-by-default": "{value} par défaut",
|
||||
"value-from": "Valeur issue de",
|
||||
"vcpu": "vCPU",
|
||||
"vcpu-at-startup": "vCPU au démarrage",
|
||||
"vcpus": "vCPUs",
|
||||
@@ -1487,6 +1648,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",
|
||||
@@ -1527,6 +1689,7 @@
|
||||
"vms-status": "Statut des VMs",
|
||||
"vms-tags": "Tags des VMs",
|
||||
"vms-treeview": "Arborescence des VMs",
|
||||
"vmware": "VMware",
|
||||
"vtpm": "VTPM",
|
||||
"warning": "Avertissement",
|
||||
"warning:vdi-already-attached": "Le VDI sélectionné est déjà attaché à cette 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,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { InputWrapperMessage } from '@core/components/input-wrapper/VtsInputWrapper.vue'
|
||||
import { toArray } from '@core/utils/to-array.utils.ts'
|
||||
|
||||
export function mergeInputWrapperMessages(...messages: (InputWrapperMessage | undefined)[]): InputWrapperMessage {
|
||||
return messages.flatMap(message => toArray(message))
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
VITE_XO_REST_HOST=
|
||||
# Serve the V2V REST routes from dev/v2v-mock until the back end provides them
|
||||
VITE_XO_V2V_MOCK=
|
||||
# Any text makes the mocked vectura prerequisite check fail with it
|
||||
VITE_XO_V2V_MOCK_VECTURA_ERROR=
|
||||
|
||||
156
@xen-orchestra/web/dev/v2v-mock/fixtures.ts
Normal file
156
@xen-orchestra/web/dev/v2v-mock/fixtures.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
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,
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
||||
export function createMockInventory(host: string): XoV2vInventory {
|
||||
const isVcenter = host.includes('vcenter')
|
||||
|
||||
return createV2vInventory({
|
||||
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)),
|
||||
})
|
||||
}
|
||||
168
@xen-orchestra/web/dev/v2v-mock/plugin.ts
Normal file
168
@xen-orchestra/web/dev/v2v-mock/plugin.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import type { XoV2vInventoryPayload, XoV2vStartPayload, XoV2vStatus, XoV2vVm } from '@vates/types'
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http'
|
||||
import type { Plugin } from 'vite'
|
||||
import { createMockInventory } from './fixtures.ts'
|
||||
|
||||
type V2vMockOptions = {
|
||||
enabled: boolean
|
||||
vecturaError?: string
|
||||
}
|
||||
|
||||
function sendJson(response: ServerResponse, status: number, body: unknown) {
|
||||
response.statusCode = status
|
||||
response.setHeader('Content-Type', 'application/json')
|
||||
response.end(JSON.stringify(body))
|
||||
}
|
||||
|
||||
async function readJson<T>(request: IncomingMessage): Promise<T> {
|
||||
const chunks: Buffer[] = []
|
||||
|
||||
for await (const chunk of request) {
|
||||
chunks.push(chunk as Buffer)
|
||||
}
|
||||
|
||||
return JSON.parse(Buffer.concat(chunks).toString('utf8')) as T
|
||||
}
|
||||
|
||||
async function handleInventory(request: IncomingMessage, response: ServerResponse) {
|
||||
const { connection } = await readJson<XoV2vInventoryPayload>(request)
|
||||
|
||||
if (connection.password === 'bad') {
|
||||
sendJson(response, 422, { error: 'invalid credentials', data: { code: 'V2V_INVALID_CREDENTIALS' } })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (connection.host === 'unreachable.lab') {
|
||||
sendJson(response, 500, { error: `${connection.host}:${connection.port} is unreachable` })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// A real inventory takes a while: let the "connecting" state show
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
|
||||
sendJson(response, 200, createMockInventory(connection.host))
|
||||
}
|
||||
|
||||
type V2vStartProblem = { vm?: XoV2vVm['id']; field?: string; message: string }
|
||||
|
||||
const START_VM_UNSUPPORTED = 'starting the VM after its import is not supported yet'
|
||||
|
||||
const CLEANUP_UNSUPPORTED = 'cleaning up the VM is not supported yet'
|
||||
|
||||
// Mirrors `planMigration` of the REST API (`@xen-orchestra/rest-api/src/v2v/v2v.plan.mts`): every problem at once
|
||||
function planMockMigration({ connection, defaults, vms }: XoV2vStartPayload): V2vStartProblem[] {
|
||||
const problems: V2vStartProblem[] = []
|
||||
const sourceVms = new Map(createMockInventory(connection.host).vms.map(vm => [vm.id, vm]))
|
||||
const seenVmIds = new Set<XoV2vVm['id']>()
|
||||
|
||||
if (defaults.startVm) {
|
||||
problems.push({ field: 'defaults.startVm', message: START_VM_UNSUPPORTED })
|
||||
}
|
||||
|
||||
if (defaults.cleanup) {
|
||||
problems.push({ field: 'defaults.cleanup', message: CLEANUP_UNSUPPORTED })
|
||||
}
|
||||
|
||||
for (const vm of vms) {
|
||||
const report = (message: string, field?: string) => problems.push({ vm: vm.id, field, message })
|
||||
|
||||
if (seenVmIds.has(vm.id)) {
|
||||
report('the VM is listed more than once')
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
seenVmIds.add(vm.id)
|
||||
|
||||
const sourceVm = sourceVms.get(vm.id)
|
||||
|
||||
if (sourceVm === undefined) {
|
||||
report('no such VM on the source')
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (vm.startVm) {
|
||||
report(START_VM_UNSUPPORTED, 'startVm')
|
||||
}
|
||||
|
||||
if (vm.cleanup) {
|
||||
report(CLEANUP_UNSUPPORTED, 'cleanup')
|
||||
}
|
||||
|
||||
if (vm.tags.length > 0) {
|
||||
report('setting tags is not supported yet', 'tags')
|
||||
}
|
||||
|
||||
if (!vm.keepMac) {
|
||||
report('the MAC addresses are always kept for now', 'keepMac')
|
||||
}
|
||||
|
||||
for (const disk of sourceVm.disks) {
|
||||
if (disk.isRdm) {
|
||||
report(`the disk ${disk.key} is a raw device mapping, which cannot be imported`, 'disks')
|
||||
}
|
||||
|
||||
if (disk.isEncrypted) {
|
||||
report(`the disk ${disk.key} is encrypted, which cannot be imported`, 'disks')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return problems
|
||||
}
|
||||
|
||||
async function handleStart(request: IncomingMessage, response: ServerResponse) {
|
||||
const problems = planMockMigration(await readJson<XoV2vStartPayload>(request))
|
||||
|
||||
if (problems.length > 0) {
|
||||
// What the REST API error handler answers for `invalidParameters`
|
||||
sendJson(response, 422, { error: 'the migration cannot be started', data: { errors: problems } })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// The task does not exist in XO: its link leads to an empty task panel
|
||||
sendJson(response, 202, { taskId: `v2v-mock-task-${Date.now()}` })
|
||||
}
|
||||
|
||||
const START_PATHNAME = /^\/rest\/v0\/pools\/[^/]+\/actions\/import_vms_from_vmware$/
|
||||
|
||||
// Stands in for the V2V REST routes until the back end ships them (dev server only, never built)
|
||||
export function v2vMockPlugin({ enabled, vecturaError }: V2vMockOptions): Plugin {
|
||||
return {
|
||||
name: 'xo-v2v-mock',
|
||||
apply: (_config, { command }) => enabled && command === 'serve',
|
||||
configureServer(server) {
|
||||
server.middlewares.use((request, response, next) => {
|
||||
const { pathname } = new URL(request.url ?? '/', 'http://localhost')
|
||||
|
||||
if (pathname === '/rest/v0/v2v/status' && request.method === 'GET') {
|
||||
const status: XoV2vStatus = {
|
||||
vectura: vecturaError ? { status: 'error', error: vecturaError } : { status: 'success' },
|
||||
}
|
||||
|
||||
sendJson(response, 200, status)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (pathname === '/rest/v0/v2v/inventory' && request.method === 'POST') {
|
||||
handleInventory(request, response).catch(next)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (START_PATHNAME.test(pathname) && request.method === 'POST') {
|
||||
handleStart(request, response).catch(next)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -38,7 +38,7 @@ Those types are declared in `tsconfig.vitest.json`, not in `tsconfig.app.json`,
|
||||
|
||||
The two projects split on the file name:
|
||||
|
||||
| Project | Covers | Test globals |
|
||||
| ---------------------- | ----------------------------------------------------- | ------------ |
|
||||
| `tsconfig.app.json` | everything **except** `*.unit.test.ts` and `setup.ts` | no |
|
||||
| `tsconfig.vitest.json` | everything, tests included | yes |
|
||||
| Project | Covers | Test globals |
|
||||
| ---------------------- | ------------------------------------------------------ | ------------ |
|
||||
| `tsconfig.app.json` | everything **except** `*.unit.test.ts` and `src/test/` | no |
|
||||
| `tsconfig.vitest.json` | everything, tests included | yes |
|
||||
|
||||
@@ -22,6 +22,35 @@ beforeEach(() => {
|
||||
|
||||
Note the cast: the typed form checks the factory return against the **whole** module, so a partial mock of a large export needs `as unknown as typeof useX` (with an `import type { useX }`).
|
||||
|
||||
When several test files mock the same remote resource (a collection or a single resource) with the same shape, the mock lives once in `src/test/` and the test file only points `vi.mock` to it; its exported state is read and changed by the tests, and reset in `beforeEach`:
|
||||
|
||||
```typescript
|
||||
vi.mock(
|
||||
import('@/modules/vm/remote-resources/use-xo-vm-collection.ts'),
|
||||
() => import('@/test/mock-xo-vm-collection.ts')
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
vmCollectionMock.reset()
|
||||
})
|
||||
```
|
||||
|
||||
`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
|
||||
|
||||
`src/test/setup.ts` stubs the global `fetch` with a never-settling promise, so composables that trigger remote requests on use never hit the network in unit tests. Override it locally with `vi.stubGlobal('fetch', …)` when a test needs a real response.
|
||||
`src/test/setup.ts` stubs the global `fetch` with a never-settling promise before each test, so composables that trigger remote requests on use never hit the network in unit tests. When a test needs a real response, `stubFetchResponse(status, body)` from `src/test/stub-fetch-response.ts` overrides it for that test only and returns the mock, to assert the request when the request is the contract (a job). `stubFetchServerError()` answers a 500 the server explains with `SERVER_ERROR_MESSAGE`; `stubV2vStartAccepted()` (the V2V start route answering `V2V_START_TASK_ID`) and `stubV2vStartProblems(errors)` (refusing the launch for those problems) live in `create-v2v-launch.ts`.
|
||||
|
||||
@@ -16,17 +16,21 @@ it('is true while a state-changing operation is pending', () => {
|
||||
})
|
||||
```
|
||||
|
||||
Two cases legitimately stay inline: a **reactivity** test, which needs a `ref` it can reassign, and a test that must call `useI18n()` _inside_ the `setup` callback to build its expected value (see [Testing composables](./testing-composables.md)).
|
||||
A helper returning `wrapper.vm` serves a **reactivity** test too, as long as it accepts the source as a `ref` (`MaybeRefOrGetter`): `wrapper.vm` keeps following the updates. Only a test that must call a function returned by the composable, or build its expected value, _inside_ the `setup` callback stays inline (see [Testing composables](./testing-composables.md)).
|
||||
|
||||
Because mocks are read lazily, calling `getX.mockReturnValue(...)` in the test body _before_ invoking the helper still takes effect.
|
||||
|
||||
This applies to **navigation into the result** too, not only to mounting. An expression such as `result.filterableServers[0]`, repeated across a `describe` block, is repeated setup — give it a helper and assert on a named local, rather than re-deriving it in every test.
|
||||
|
||||
The exception is a **reactivity** test: it must keep the live `wrapper` so it can re-read through `wrapper.vm` after reassigning the source ref. A helper returning a snapshot of the first item would defeat the test.
|
||||
The exception is a **reactivity** test: it must keep reading through the mounted `vm` after reassigning the source ref. A helper returning a snapshot of the first item would defeat the test.
|
||||
|
||||
## 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`
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Testing components
|
||||
|
||||
Components run in the same `unit` project as everything else: `@vue/test-utils` `mount` on `happy-dom`, **no browser needed**. Mount with `createGlobalTestConfig()` so the real `vue-i18n` instance and a fresh Pinia are installed, and give the repeated mount a helper (see [Setup](./setup-helpers.md)):
|
||||
Components run in the same `unit` project as everything else: `@vue/test-utils` `mount` on `happy-dom`, **no browser needed**. Mount with `createGlobalTestConfig()` so the real `vue-i18n` instance, the app form validation messages (`RegleVuePlugin` with `formValidationConfig`, as in `main.ts`) and a fresh Pinia are installed, and give the repeated mount a helper (see [Setup](./setup-helpers.md)):
|
||||
|
||||
```typescript
|
||||
import VmSystemGraphics from '@/modules/vm/components/system/VmSystemGraphics.vue'
|
||||
@@ -61,6 +61,8 @@ 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.
|
||||
|
||||
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:
|
||||
|
||||
| Helper | Reads |
|
||||
@@ -131,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:
|
||||
@@ -225,6 +246,13 @@ expect(
|
||||
|
||||
Pushing before mounting is what makes the active tab assertable; without a push the router sits at its start location and nothing is active.
|
||||
|
||||
## Selects
|
||||
|
||||
How `FormSelect` and `VtsSelect` render and open their options belongs to the web-core tests, never to an XO 6 one:
|
||||
|
||||
- A component that only passes select bindings on is mounted with `createGlobalTestConfig({ stubs: { FormSelect: true } })`, given `createSelectBindings`, and read with `findSelectStubLabels`.
|
||||
- The options a form builds (their labels, their custom properties) are read by mounting the form with `mountWithSelect`, rendering a `VtsSelect` for its select, then `openSelect` and `findSelectOptionLabels`; `findShownSelections` reads the selection each select shows.
|
||||
|
||||
## What `happy-dom` cannot do
|
||||
|
||||
There is no layout engine, and Vitest's `css` option is off, so component styles are never loaded. Out of reach:
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
# Testing composables
|
||||
|
||||
A composable that relies on a Vue context (`useI18n`, `inject`, an effect scope, plugins) must run **inside a mounted component**. Use the `mountComposable` helper from `src/test/mount-composable.ts`, which mounts a throwaway component with the real `vue-i18n` instance and a fresh Pinia:
|
||||
A composable that relies on a Vue context (`useI18n`, `inject`, an effect scope, plugins) must run **inside a mounted component**. Use the helpers from `src/test/mount-composable.ts`, which mount a throwaway component with `createGlobalTestConfig()`: the real `vue-i18n` instance, the app form validation messages, a fresh Pinia and a router.
|
||||
|
||||
Because the real i18n instance is installed, translations resolve to actual strings, and the assertion builds its expected label with `t` instead of hard-coding the wording — see [Assert behaviour](./assert-behaviour.md). `mountComposableWithI18n` mounts the composable, spreads its result, adds the `t` of the mounted component and returns `wrapper.vm`:
|
||||
|
||||
```typescript
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { mountComposableWithI18n } from '@/test/mount-composable.ts'
|
||||
|
||||
const { wrapper } = mountComposable(() => {
|
||||
const { t } = useI18n()
|
||||
const result = mountComposableWithI18n(() => useXoVmUtils(createVm({ power_state: VM_POWER_STATE.RUNNING })))
|
||||
|
||||
return { ...useXoVmUtils(createVm({ power_state: VM_POWER_STATE.RUNNING })), t }
|
||||
})
|
||||
|
||||
expect(wrapper.vm.powerState.icon).toBe('status:running-circle')
|
||||
expect(wrapper.vm.powerState.text).toBe(wrapper.vm.t('vm:status:running'))
|
||||
expect(result.powerState.icon).toBe('status:running-circle')
|
||||
expect(result.powerState.text).toBe(result.t('vm:status:running'))
|
||||
```
|
||||
|
||||
The composable result becomes the component state, so `wrapper.vm` exposes it with refs **already unwrapped** — no `.value`. Read through `wrapper.vm` rather than destructuring it: a destructured value is a snapshot and stops following updates.
|
||||
`mountComposable` does the same without `t` and returns `{ wrapper }`, for a test that needs neither the translation nor a returned object spread.
|
||||
|
||||
Because the real i18n instance is installed, translations resolve to actual strings. Return `t` from the `setup` callback so the assertion can build its expected label with it instead of hard-coding the wording — see [Assert behaviour](./assert-behaviour.md).
|
||||
Both take the options of `createGlobalTestConfig()` as a second argument, e.g. `{ router }` for a composable that navigates (build it with `createTestRouter()` and assert on `router.currentRoute.value`), or `{ provide }` for a composable injecting what a parent component provides.
|
||||
|
||||
The composable result becomes the component state, so `wrapper.vm` exposes it with refs **already unwrapped** — no `.value`. Read through it rather than destructuring it: a destructured value is a snapshot and stops following updates.
|
||||
|
||||
The wrapper is unmounted after the test, so `onUnmounted` and `onScopeDispose` cleanup runs and no watcher of a previous test can re-fire in the next one.
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"vite": "^8.0.0",
|
||||
"vitest": "^4.1.9",
|
||||
"vue": "~3.5.13",
|
||||
"vue-component-type-helpers": "~3.3.7",
|
||||
"vue-i18n": "^11.1.2",
|
||||
"vue-router": "5.0.6",
|
||||
"vue-tsc": "^3.0.0",
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import AdministrationMenu from '@/modules/admin/components/AdministrationMenu.vue'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { type DOMWrapper, mount } from '@vue/test-utils'
|
||||
|
||||
function mountMenu() {
|
||||
return mount(AdministrationMenu, { global: createGlobalTestConfig() })
|
||||
}
|
||||
|
||||
function findSections(wrapper: ReturnType<typeof mountMenu>) {
|
||||
return wrapper.findAll('.menu-section')
|
||||
}
|
||||
|
||||
function findSectionTitle(section: DOMWrapper<Element>) {
|
||||
return section.get('.section-title').text()
|
||||
}
|
||||
|
||||
function findSection(wrapper: ReturnType<typeof mountMenu>, title: string) {
|
||||
return findSections(wrapper).find(section => findSectionTitle(section) === title)
|
||||
}
|
||||
|
||||
it('lists the V2V migration between backups and user management', () => {
|
||||
expect(findSections(mountMenu()).map(findSectionTitle)).toEqual([
|
||||
t('backup-and-replication'),
|
||||
t('v2v'),
|
||||
t('user-management'),
|
||||
t('settings'),
|
||||
])
|
||||
})
|
||||
|
||||
it('links the V2V section to the migration wizard', () => {
|
||||
const section = findSection(mountMenu(), t('v2v'))
|
||||
|
||||
expect(section?.findAll('a').map(link => [link.text(), link.attributes('href')])).toEqual([
|
||||
[t('migrate-vms'), '/admin/v2v/migrate'],
|
||||
])
|
||||
})
|
||||
@@ -58,6 +58,10 @@ const sections = computed<AdminMenuSection[]>(() => [
|
||||
{ label: t('logs'), href: undefined, to: { name: '/admin/backup-and-replication/logs' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t('v2v'),
|
||||
items: [{ label: t('migrate-vms'), href: undefined, to: { name: '/admin/v2v/migrate' } }],
|
||||
},
|
||||
{
|
||||
title: t('user-management'),
|
||||
items: [
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import BulkEditBackupRepositoriesDrawer from '@/modules/backup-repository/components/drawer/BulkEditBackupRepositoriesDrawer.vue'
|
||||
import BackupRepositoryFormCheckbox from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormCheckbox.vue'
|
||||
import BackupRepositoryDetailsStep from '@/modules/backup-repository/components/form/steps/BackupRepositoryDetailsStep.vue'
|
||||
import BackupRepositoryGeneralStep from '@/modules/backup-repository/components/form/steps/BackupRepositoryGeneralStep.vue'
|
||||
import type { BulkEditBackupRepositoryPayloads } from '@/modules/backup-repository/jobs/xo-backup-repository-bulk-edit.job.ts'
|
||||
import type { FrontXoBackupRepository } from '@/modules/backup-repository/remote-resources/use-xo-backup-repository-collection.ts'
|
||||
import type { useXoProxyCollection } from '@/modules/proxy/remote-resources/use-xo-proxy-collection.ts'
|
||||
import FormCheckbox from '@/shared/components/form/FormCheckbox.vue'
|
||||
import { createBr } from '@/test/create-br.ts'
|
||||
import { submitDrawer } from '@/test/drawer.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
@@ -72,7 +72,7 @@ it('shows the general information without the encryption', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
|
||||
expect(wrapper.findComponent(BackupRepositoryGeneralStep).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(BackupRepositoryFormCheckbox).exists()).toBe(false)
|
||||
expect(wrapper.findComponent(FormCheckbox).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows the details of the repositories when they are of the same type', async () => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
<template>
|
||||
<div class="backup-repository-azure-fields">
|
||||
<BackupRepositoryFormRow>
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.hostName" />
|
||||
<BackupRepositoryFormCheckbox v-if="isAzurite" v-bind="bindings.useHttps" />
|
||||
<FormTextInput v-bind="bindings.hostName" />
|
||||
<FormCheckbox v-if="isAzurite" v-bind="bindings.useHttps" />
|
||||
</BackupRepositoryFormRow>
|
||||
|
||||
<BackupRepositoryFormRow>
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.accountName" />
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.key" />
|
||||
<FormTextInput v-bind="bindings.accountName" />
|
||||
<FormTextInput v-bind="bindings.key" />
|
||||
</BackupRepositoryFormRow>
|
||||
|
||||
<BackupRepositoryFormRow>
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.containerName" />
|
||||
<FormTextInput v-bind="bindings.containerName" />
|
||||
</BackupRepositoryFormRow>
|
||||
|
||||
<BackupRepositoryFormRow wide>
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.pathInContainer" />
|
||||
<FormTextInput v-bind="bindings.pathInContainer" />
|
||||
</BackupRepositoryFormRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BackupRepositoryFormRow from '@/modules/backup-repository/components/form/BackupRepositoryFormRow.vue'
|
||||
import BackupRepositoryFormCheckbox from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormCheckbox.vue'
|
||||
import BackupRepositoryFormTextInput from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormTextInput.vue'
|
||||
import type { AzureBackupRepositoryDetailsForm } from '@/modules/backup-repository/form/details/use-azure-backup-repository-details-form.ts'
|
||||
import FormCheckbox from '@/shared/components/form/FormCheckbox.vue'
|
||||
import FormTextInput from '@/shared/components/form/FormTextInput.vue'
|
||||
|
||||
defineProps<{
|
||||
bindings: AzureBackupRepositoryDetailsForm['bindings']
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<BackupRepositoryFormRow wide>
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.path" />
|
||||
<FormTextInput v-bind="bindings.path" />
|
||||
</BackupRepositoryFormRow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BackupRepositoryFormRow from '@/modules/backup-repository/components/form/BackupRepositoryFormRow.vue'
|
||||
import BackupRepositoryFormTextInput from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormTextInput.vue'
|
||||
import type { LocalBackupRepositoryDetailsForm } from '@/modules/backup-repository/form/details/use-local-backup-repository-details-form.ts'
|
||||
import FormTextInput from '@/shared/components/form/FormTextInput.vue'
|
||||
|
||||
defineProps<{
|
||||
bindings: LocalBackupRepositoryDetailsForm['bindings']
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="backup-repository-nfs-fields">
|
||||
<BackupRepositoryFormRow>
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.host" />
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.port" />
|
||||
<FormTextInput v-bind="bindings.host" />
|
||||
<FormTextInput v-bind="bindings.port" />
|
||||
</BackupRepositoryFormRow>
|
||||
|
||||
<BackupRepositoryFormRow wide>
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.path" />
|
||||
<FormTextInput v-bind="bindings.path" />
|
||||
</BackupRepositoryFormRow>
|
||||
|
||||
<BackupRepositoryFormRow wide>
|
||||
@@ -18,8 +18,8 @@
|
||||
<script lang="ts" setup>
|
||||
import BackupRepositoryFormRow from '@/modules/backup-repository/components/form/BackupRepositoryFormRow.vue'
|
||||
import BackupRepositoryFormTextArea from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormTextArea.vue'
|
||||
import BackupRepositoryFormTextInput from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormTextInput.vue'
|
||||
import type { NfsBackupRepositoryDetailsForm } from '@/modules/backup-repository/form/details/use-nfs-backup-repository-details-form.ts'
|
||||
import FormTextInput from '@/shared/components/form/FormTextInput.vue'
|
||||
|
||||
defineProps<{ bindings: NfsBackupRepositoryDetailsForm['bindings'] }>()
|
||||
</script>
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
<template>
|
||||
<div class="backup-repository-s3-fields">
|
||||
<div class="endpoint-row">
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.endpoint" class="endpoint" />
|
||||
<FormTextInput v-bind="bindings.endpoint" class="endpoint" />
|
||||
<div class="checkboxes">
|
||||
<BackupRepositoryFormCheckbox v-bind="bindings.useHttps" />
|
||||
<BackupRepositoryFormCheckbox v-if="bindings.useHttps.modelValue" v-bind="bindings.allowUnauthorized" />
|
||||
<FormCheckbox v-bind="bindings.useHttps" />
|
||||
<FormCheckbox v-if="bindings.useHttps.modelValue" v-bind="bindings.allowUnauthorized" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BackupRepositoryFormRow>
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.region" />
|
||||
<FormTextInput v-bind="bindings.region" />
|
||||
</BackupRepositoryFormRow>
|
||||
|
||||
<BackupRepositoryFormRow>
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.accessKeyId" />
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.secret" />
|
||||
<FormTextInput v-bind="bindings.accessKeyId" />
|
||||
<FormTextInput v-bind="bindings.secret" />
|
||||
</BackupRepositoryFormRow>
|
||||
|
||||
<BackupRepositoryFormRow>
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.bucket" />
|
||||
<FormTextInput v-bind="bindings.bucket" />
|
||||
</BackupRepositoryFormRow>
|
||||
|
||||
<BackupRepositoryFormRow wide>
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.pathInBucket" />
|
||||
<FormTextInput v-bind="bindings.pathInBucket" />
|
||||
</BackupRepositoryFormRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BackupRepositoryFormRow from '@/modules/backup-repository/components/form/BackupRepositoryFormRow.vue'
|
||||
import BackupRepositoryFormCheckbox from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormCheckbox.vue'
|
||||
import BackupRepositoryFormTextInput from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormTextInput.vue'
|
||||
import type { S3BackupRepositoryDetailsForm } from '@/modules/backup-repository/form/details/use-s3-backup-repository-details-form.ts'
|
||||
import FormCheckbox from '@/shared/components/form/FormCheckbox.vue'
|
||||
import FormTextInput from '@/shared/components/form/FormTextInput.vue'
|
||||
|
||||
defineProps<{ bindings: S3BackupRepositoryDetailsForm['bindings'] }>()
|
||||
</script>
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
</UiAlert>
|
||||
|
||||
<BackupRepositoryFormRow>
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.pathOnShare" />
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.subfolder" />
|
||||
<FormTextInput v-bind="bindings.pathOnShare" />
|
||||
<FormTextInput v-bind="bindings.subfolder" />
|
||||
</BackupRepositoryFormRow>
|
||||
|
||||
<BackupRepositoryFormRow>
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.username" />
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.password" />
|
||||
<FormTextInput v-bind="bindings.username" />
|
||||
<FormTextInput v-bind="bindings.password" />
|
||||
</BackupRepositoryFormRow>
|
||||
|
||||
<BackupRepositoryFormRow>
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.domain" />
|
||||
<FormTextInput v-bind="bindings.domain" />
|
||||
</BackupRepositoryFormRow>
|
||||
|
||||
<BackupRepositoryFormRow wide>
|
||||
@@ -27,8 +27,8 @@
|
||||
<script lang="ts" setup>
|
||||
import BackupRepositoryFormRow from '@/modules/backup-repository/components/form/BackupRepositoryFormRow.vue'
|
||||
import BackupRepositoryFormTextArea from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormTextArea.vue'
|
||||
import BackupRepositoryFormTextInput from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormTextInput.vue'
|
||||
import type { SmbBackupRepositoryDetailsForm } from '@/modules/backup-repository/form/details/use-smb-backup-repository-details-form.ts'
|
||||
import FormTextInput from '@/shared/components/form/FormTextInput.vue'
|
||||
import UiAlert from '@core/components/ui/alert/UiAlert.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import BackupRepositoryFormCheckbox from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormCheckbox.vue'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||
|
||||
type CheckboxProps = InstanceType<typeof BackupRepositoryFormCheckbox>['$props']
|
||||
|
||||
function mountCheckbox(props: Partial<CheckboxProps> = {}) {
|
||||
return mount(BackupRepositoryFormCheckbox, {
|
||||
props: { label: 'Encrypted', modelValue: false, ...props },
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
function findWarnings(wrapper: VueWrapper) {
|
||||
return wrapper.findAll('.ui-info').map(warning => warning.text())
|
||||
}
|
||||
|
||||
it('shows its label', () => {
|
||||
expect(mountCheckbox().get('.checkbox-label').text()).toBe('Encrypted')
|
||||
})
|
||||
|
||||
it('shows no warning when none is given', () => {
|
||||
expect(findWarnings(mountCheckbox())).toEqual([])
|
||||
})
|
||||
|
||||
it.each<[string, CheckboxProps['warning']]>([
|
||||
['a text', 'The data cannot be recovered'],
|
||||
['a message', { content: 'The data cannot be recovered', accent: 'warning' }],
|
||||
])('shows the warning given as %s', (_, warning) => {
|
||||
expect(findWarnings(mountCheckbox({ warning }))).toEqual(['The data cannot be recovered'])
|
||||
})
|
||||
|
||||
it('shows every warning when several are given', () => {
|
||||
const warning = ['The data cannot be recovered', 'Another warning']
|
||||
|
||||
expect(findWarnings(mountCheckbox({ warning }))).toEqual(warning)
|
||||
})
|
||||
|
||||
it('is checked according to its model', () => {
|
||||
const isChecked = (modelValue: boolean) =>
|
||||
mountCheckbox({ modelValue }).get<HTMLInputElement>('input[type="checkbox"]').element.checked
|
||||
|
||||
expect({ whenFalse: isChecked(false), whenTrue: isChecked(true) }).toEqual({ whenFalse: false, whenTrue: true })
|
||||
})
|
||||
|
||||
it('is indeterminate when its model is undefined, as for a value which differs between edited repositories', () => {
|
||||
expect(mountCheckbox({ modelValue: undefined }).find('input.indeterminate').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('updates its model when it is checked', async () => {
|
||||
const wrapper = mountCheckbox()
|
||||
|
||||
await wrapper.get('input[type="checkbox"]').setValue(true)
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')).toEqual([[true]])
|
||||
})
|
||||
|
||||
it('cannot be checked when it is disabled', () => {
|
||||
const wrapper = mountCheckbox({ disabled: true })
|
||||
|
||||
expect(wrapper.get('input[type="checkbox"]').attributes()).toHaveProperty('disabled')
|
||||
})
|
||||
@@ -1,67 +0,0 @@
|
||||
import BackupRepositoryFormTextInput from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormTextInput.vue'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||
|
||||
type TextInputProps = InstanceType<typeof BackupRepositoryFormTextInput>['$props']
|
||||
|
||||
function mountTextInput(props: Partial<TextInputProps> = {}) {
|
||||
return mount(BackupRepositoryFormTextInput, {
|
||||
props: { label: 'Host', modelValue: '', ...props },
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
function findMessages(wrapper: VueWrapper) {
|
||||
return wrapper.findAll('.ui-info').map(message => message.text())
|
||||
}
|
||||
|
||||
it('shows its label', () => {
|
||||
expect(mountTextInput().get('.ui-label').text()).toContain('Host')
|
||||
})
|
||||
|
||||
it('shows no message without info nor error', () => {
|
||||
expect(findMessages(mountTextInput())).toEqual([])
|
||||
})
|
||||
|
||||
it('shows the info as its message', () => {
|
||||
const wrapper = mountTextInput({ info: 'Example: 192.168.1.10' })
|
||||
|
||||
expect(findMessages(wrapper)).toEqual(['Example: 192.168.1.10'])
|
||||
})
|
||||
|
||||
it('shows the error instead of the info', () => {
|
||||
const wrapper = mountTextInput({
|
||||
info: 'Example: 192.168.1.10',
|
||||
error: { content: 'This field is required', accent: 'danger' },
|
||||
})
|
||||
|
||||
expect(findMessages(wrapper)).toEqual(['This field is required'])
|
||||
})
|
||||
|
||||
it('shows the prefix ahead of the value', () => {
|
||||
expect(mountTextInput({ prefix: '\\\\' }).get('.prefix').text()).toBe('\\\\')
|
||||
})
|
||||
|
||||
it('disables the input when it is disabled', () => {
|
||||
expect(mountTextInput({ disabled: true }).get('input').attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('enables the input by default', () => {
|
||||
expect(mountTextInput().get('input').attributes('disabled')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('updates its model with the trimmed value', async () => {
|
||||
const wrapper = mountTextInput()
|
||||
|
||||
await wrapper.get('input').setValue(' 192.168.1.10 ')
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')).toEqual([['192.168.1.10']])
|
||||
})
|
||||
|
||||
it('reports that the input lost the focus', async () => {
|
||||
const wrapper = mountTextInput()
|
||||
|
||||
await wrapper.get('input').trigger('blur')
|
||||
|
||||
expect(wrapper.emitted('blur')).toHaveLength(1)
|
||||
})
|
||||
@@ -1,13 +1,13 @@
|
||||
import BackupRepositoryFormRow from '@/modules/backup-repository/components/form/BackupRepositoryFormRow.vue'
|
||||
import BackupRepositoryFormCheckbox from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormCheckbox.vue'
|
||||
import BackupRepositoryFormSelect from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormSelect.vue'
|
||||
import BackupRepositoryFormTextInput from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormTextInput.vue'
|
||||
import BackupRepositoryGeneralStep from '@/modules/backup-repository/components/form/steps/BackupRepositoryGeneralStep.vue'
|
||||
import {
|
||||
type BackupRepositoryGeneralFormData,
|
||||
useBackupRepositoryGeneralForm,
|
||||
} from '@/modules/backup-repository/form/use-backup-repository-general-form.ts'
|
||||
import type { useXoProxyCollection } from '@/modules/proxy/remote-resources/use-xo-proxy-collection.ts'
|
||||
import FormCheckbox from '@/shared/components/form/FormCheckbox.vue'
|
||||
import FormTextInput from '@/shared/components/form/FormTextInput.vue'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
@@ -40,10 +40,7 @@ function mountGeneralStep(formData: Partial<BackupRepositoryGeneralFormData> = {
|
||||
return { wrapper, data }
|
||||
}
|
||||
|
||||
function findLabels(
|
||||
wrapper: VueWrapper,
|
||||
component: typeof BackupRepositoryFormSelect | typeof BackupRepositoryFormTextInput
|
||||
) {
|
||||
function findLabels(wrapper: VueWrapper, component: typeof BackupRepositoryFormSelect | typeof FormTextInput) {
|
||||
return wrapper.findAllComponents(component).map(input => input.props('label'))
|
||||
}
|
||||
|
||||
@@ -61,13 +58,13 @@ it('hides the options section, and its encryption, when asked to', () => {
|
||||
const { wrapper } = mountGeneralStep({}, { hideEncryption: true })
|
||||
|
||||
expect(findSectionTitles(wrapper)).toEqual([t('general-information')])
|
||||
expect(wrapper.findComponent(BackupRepositoryFormCheckbox).exists()).toBe(false)
|
||||
expect(wrapper.findComponent(FormCheckbox).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('offers the name, type, backup format and proxy fields', () => {
|
||||
const { wrapper } = mountGeneralStep()
|
||||
|
||||
expect(findLabels(wrapper, BackupRepositoryFormTextInput)).toEqual([t('name')])
|
||||
expect(findLabels(wrapper, FormTextInput)).toEqual([t('name')])
|
||||
expect(findLabels(wrapper, BackupRepositoryFormSelect)).toEqual([t('type'), t('backup-format'), t('proxy')])
|
||||
})
|
||||
|
||||
@@ -80,8 +77,8 @@ it('warns that the encryption is available for the block based format only', ()
|
||||
it('hides the encryption key until the encryption is enabled', () => {
|
||||
const { wrapper } = mountGeneralStep({ type: 's3', backupFormat: 'block' })
|
||||
|
||||
expect(wrapper.findComponent(BackupRepositoryFormCheckbox).exists()).toBe(true)
|
||||
expect(findLabels(wrapper, BackupRepositoryFormTextInput)).not.toContain(t('key'))
|
||||
expect(wrapper.findComponent(FormCheckbox).exists()).toBe(true)
|
||||
expect(findLabels(wrapper, FormTextInput)).not.toContain(t('key'))
|
||||
})
|
||||
|
||||
it('shows the encryption key on the same row as the encryption, once enabled', async () => {
|
||||
@@ -92,9 +89,9 @@ it('shows the encryption key on the same row as the encryption, once enabled', a
|
||||
|
||||
const encryptionRow = wrapper
|
||||
.findAllComponents(BackupRepositoryFormRow)
|
||||
.find(row => row.findComponent(BackupRepositoryFormCheckbox).exists())
|
||||
.find(row => row.findComponent(FormCheckbox).exists())
|
||||
|
||||
expect(findLabels(encryptionRow!, BackupRepositoryFormTextInput)).toEqual([t('key')])
|
||||
expect(findLabels(encryptionRow!, FormTextInput)).toEqual([t('key')])
|
||||
})
|
||||
|
||||
it('describes each backup format with its hint', async () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<UiTitle>{{ t('general-information') }}</UiTitle>
|
||||
|
||||
<BackupRepositoryFormRow>
|
||||
<BackupRepositoryFormTextInput v-bind="bindings.name" />
|
||||
<FormTextInput v-bind="bindings.name" />
|
||||
</BackupRepositoryFormRow>
|
||||
|
||||
<BackupRepositoryFormRow>
|
||||
@@ -36,8 +36,8 @@
|
||||
</UiAlert>
|
||||
|
||||
<BackupRepositoryFormRow>
|
||||
<BackupRepositoryFormCheckbox v-bind="bindings.encrypted" />
|
||||
<BackupRepositoryFormTextInput v-if="bindings.encrypted.modelValue" v-bind="bindings.encryptionKey" />
|
||||
<FormCheckbox v-bind="bindings.encrypted" />
|
||||
<FormTextInput v-if="bindings.encrypted.modelValue" v-bind="bindings.encryptionKey" />
|
||||
</BackupRepositoryFormRow>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,10 +45,10 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BackupRepositoryFormRow from '@/modules/backup-repository/components/form/BackupRepositoryFormRow.vue'
|
||||
import BackupRepositoryFormCheckbox from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormCheckbox.vue'
|
||||
import BackupRepositoryFormSelect from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormSelect.vue'
|
||||
import BackupRepositoryFormTextInput from '@/modules/backup-repository/components/form/inputs/BackupRepositoryFormTextInput.vue'
|
||||
import type { BackupRepositoryGeneralForm } from '@/modules/backup-repository/form/use-backup-repository-general-form.ts'
|
||||
import FormCheckbox from '@/shared/components/form/FormCheckbox.vue'
|
||||
import FormTextInput from '@/shared/components/form/FormTextInput.vue'
|
||||
import VtsOption from '@core/components/select/VtsOption.vue'
|
||||
import UiAlert from '@core/components/ui/alert/UiAlert.vue'
|
||||
import UiTitle from '@core/components/ui/title/UiTitle.vue'
|
||||
|
||||
@@ -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,8 +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 type { useOverlay } from '@core/packages/overlay/use-overlay.ts'
|
||||
import { silenceConsoleError } from '@/test/silence-console-error.ts'
|
||||
import { KEEP_OVERLAY_OPEN } from '@core/packages/overlay/symbols.ts'
|
||||
import { ref, toValue } from 'vue'
|
||||
|
||||
@@ -16,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(),
|
||||
}))
|
||||
@@ -27,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()
|
||||
|
||||
@@ -70,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 }
|
||||
}
|
||||
@@ -140,7 +132,7 @@ describe('on confirm', () => {
|
||||
})
|
||||
|
||||
it('keeps the drawer open when the job fails', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
silenceConsoleError()
|
||||
run.mockRejectedValue(new Error('Missing changes for a backup repository'))
|
||||
const { editDrawer } = mountBulkEdit([firstBr])
|
||||
|
||||
|
||||
@@ -5,8 +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 type { useOverlay } from '@core/packages/overlay/use-overlay.ts'
|
||||
import { silenceConsoleError } from '@/test/silence-console-error.ts'
|
||||
import { KEEP_OVERLAY_OPEN } from '@core/packages/overlay/symbols.ts'
|
||||
import { type Ref, ref } from 'vue'
|
||||
|
||||
@@ -17,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',
|
||||
@@ -42,10 +39,9 @@ const PAYLOAD: EditBackupRepositoryPayload = {
|
||||
beforeEach(() => {
|
||||
useEditJob.mockReset()
|
||||
run.mockReset()
|
||||
useOverlayMock.mockReset()
|
||||
open.mockReset()
|
||||
overlayMock.reset()
|
||||
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
silenceConsoleError()
|
||||
|
||||
useEditJob.mockReturnValue({
|
||||
run,
|
||||
@@ -53,8 +49,6 @@ beforeEach(() => {
|
||||
isRunning: ref(false),
|
||||
errorMessage: ref(undefined),
|
||||
})
|
||||
|
||||
useOverlayMock.mockReturnValue({ open, abort: vi.fn() })
|
||||
})
|
||||
|
||||
function mountEditBackupRepository(br: FrontXoBackupRepository = createBr()) {
|
||||
@@ -62,7 +56,7 @@ function mountEditBackupRepository(br: FrontXoBackupRepository = createBr()) {
|
||||
}
|
||||
|
||||
function getOverlayOptions(): OverlayOptions {
|
||||
return useOverlayMock.mock.calls[0][0]
|
||||
return overlayMock.getOverlay<OverlayOptions>()
|
||||
}
|
||||
|
||||
function getJobArgs() {
|
||||
@@ -107,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,8 +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 type { useOverlay } from '@core/packages/overlay/use-overlay.ts'
|
||||
import { silenceConsoleError } from '@/test/silence-console-error.ts'
|
||||
import { KEEP_OVERLAY_OPEN } from '@core/packages/overlay/symbols.ts'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
@@ -15,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',
|
||||
@@ -38,13 +35,11 @@ const PAYLOAD: NewBackupRepositoryPayload = {
|
||||
beforeEach(() => {
|
||||
useCreateJob.mockReset()
|
||||
run.mockReset()
|
||||
useOverlayMock.mockReset()
|
||||
open.mockReset()
|
||||
overlayMock.reset()
|
||||
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
silenceConsoleError()
|
||||
|
||||
useCreateJob.mockReturnValue({ run })
|
||||
useOverlayMock.mockReturnValue({ open, abort: vi.fn() })
|
||||
})
|
||||
|
||||
function mountNewBackupRepository() {
|
||||
@@ -52,7 +47,7 @@ function mountNewBackupRepository() {
|
||||
}
|
||||
|
||||
function getOverlayOptions(): OverlayOptions {
|
||||
return useOverlayMock.mock.calls[0][0]
|
||||
return overlayMock.getOverlay<OverlayOptions>()
|
||||
}
|
||||
|
||||
function getJobPayload() {
|
||||
@@ -68,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', () => {
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { useXoBackupRepositoryStorageModeLabel } from '@/modules/backup-repository/composables/use-xo-backup-repository-storage-mode-label.composable.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { mountComposableWithI18n } from '@/test/mount-composable.ts'
|
||||
import { type MaybeRefOrGetter, ref } from 'vue'
|
||||
import { type ParsedBackupRepositoryUrl, parse as parseBackupRepositoryUrl } from 'xo-remote-parser'
|
||||
|
||||
function mountStorageModeLabel(parsedUrl: ParsedBackupRepositoryUrl | undefined) {
|
||||
return mountComposable(() => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return { storageModeLabel: useXoBackupRepositoryStorageModeLabel(parsedUrl), t }
|
||||
}).wrapper.vm
|
||||
function mountStorageModeLabel(parsedUrl: MaybeRefOrGetter<ParsedBackupRepositoryUrl | undefined>) {
|
||||
return mountComposableWithI18n(() => ({ storageModeLabel: useXoBackupRepositoryStorageModeLabel(parsedUrl) }))
|
||||
}
|
||||
|
||||
describe('useXoBackupRepositoryStorageModeLabel', () => {
|
||||
@@ -43,14 +38,10 @@ describe('useXoBackupRepositoryStorageModeLabel', () => {
|
||||
const parsedUrl = ref<ParsedBackupRepositoryUrl | undefined>(
|
||||
parseBackupRepositoryUrl('nfs://192.168.1.10:/exports/backups')
|
||||
)
|
||||
const { wrapper } = mountComposable(() => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return { storageModeLabel: useXoBackupRepositoryStorageModeLabel(parsedUrl), t }
|
||||
})
|
||||
const result = mountStorageModeLabel(parsedUrl)
|
||||
|
||||
parsedUrl.value = parseBackupRepositoryUrl('nfs://192.168.1.10:/exports/backups?useVhdDirectory=true')
|
||||
|
||||
expect(wrapper.vm.storageModeLabel).toBe(wrapper.vm.t('block-based'))
|
||||
expect(result.storageModeLabel).toBe(result.t('block-based'))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { useXoBackupRepositoryTypeLabel } from '@/modules/backup-repository/composables/use-xo-backup-repository-type-label.composable.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { mountComposableWithI18n } from '@/test/mount-composable.ts'
|
||||
import { type MaybeRefOrGetter, ref } from 'vue'
|
||||
import { BACKUP_REPOSITORY_TYPE, type BackupRepositoryType } from 'xo-remote-parser'
|
||||
|
||||
function mountTypeLabel(type: BackupRepositoryType | undefined) {
|
||||
return mountComposable(() => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return { typeLabel: useXoBackupRepositoryTypeLabel(type), t }
|
||||
}).wrapper.vm
|
||||
function mountTypeLabel(type: MaybeRefOrGetter<BackupRepositoryType | undefined>) {
|
||||
return mountComposableWithI18n(() => ({ typeLabel: useXoBackupRepositoryTypeLabel(type) }))
|
||||
}
|
||||
|
||||
describe('useXoBackupRepositoryTypeLabel', () => {
|
||||
@@ -41,14 +36,10 @@ describe('useXoBackupRepositoryTypeLabel', () => {
|
||||
|
||||
it('reacts to changes of the source type', () => {
|
||||
const type = ref<BackupRepositoryType | undefined>(BACKUP_REPOSITORY_TYPE.NFS)
|
||||
const { wrapper } = mountComposable(() => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return { typeLabel: useXoBackupRepositoryTypeLabel(type), t }
|
||||
})
|
||||
const result = mountTypeLabel(type)
|
||||
|
||||
type.value = BACKUP_REPOSITORY_TYPE.S3
|
||||
|
||||
expect(wrapper.vm.typeLabel).toBe(wrapper.vm.t('s3'))
|
||||
expect(result.typeLabel).toBe(result.t('s3'))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,10 +2,9 @@ import { useXoBackupRepositoryUtils } from '@/modules/backup-repository/composab
|
||||
import type { FrontXoBackupRepository } from '@/modules/backup-repository/remote-resources/use-xo-backup-repository-collection.ts'
|
||||
import type { useXoProxyCollection } from '@/modules/proxy/remote-resources/use-xo-proxy-collection.ts'
|
||||
import { createBr } from '@/test/create-br.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { mountComposableWithI18n } from '@/test/mount-composable.ts'
|
||||
import type { XoProxy } from '@vates/types'
|
||||
import { computed, ref, toValue } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { parse as parseBackupRepositoryUrl } from 'xo-remote-parser'
|
||||
|
||||
const { useGetProxyById } = vi.hoisted(() => ({
|
||||
@@ -23,11 +22,7 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
function mountUtils(br: FrontXoBackupRepository = createBr()) {
|
||||
return mountComposable(() => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return { ...useXoBackupRepositoryUtils(br, parseBackupRepositoryUrl(br.url)), t }
|
||||
}).wrapper.vm
|
||||
return mountComposableWithI18n(() => useXoBackupRepositoryUtils(br, parseBackupRepositoryUrl(br.url)))
|
||||
}
|
||||
|
||||
describe('useXoBackupRepositoryUtils', () => {
|
||||
@@ -79,18 +74,13 @@ describe('useXoBackupRepositoryUtils', () => {
|
||||
|
||||
it('reacts to changes of the source repository', () => {
|
||||
const br = ref(createBr({ url: 'nfs://192.168.100.225:/media/nfs' }))
|
||||
const { wrapper } = mountComposable(() => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
...useXoBackupRepositoryUtils(br, () => parseBackupRepositoryUrl(br.value.url)),
|
||||
t,
|
||||
}
|
||||
})
|
||||
const result = mountComposableWithI18n(() =>
|
||||
useXoBackupRepositoryUtils(br, () => parseBackupRepositoryUrl(br.value.url))
|
||||
)
|
||||
|
||||
br.value = createBr({ enabled: false, url: 's3://key:secret@s3.us-west-1.amazonaws.com/my-bucket/backups' })
|
||||
|
||||
expect(wrapper.vm.brStatus).toBe('disabled')
|
||||
expect(wrapper.vm.brType).toBe(wrapper.vm.t('s3'))
|
||||
expect(result.brStatus).toBe('disabled')
|
||||
expect(result.brType).toBe(result.t('s3'))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)]))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useAzureBackupRepositoryDetailsForm } from '@/modules/backup-repository/form/details/use-azure-backup-repository-details-form.ts'
|
||||
import { requiredMessage } from '@/test/form-messages.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import type { BackupRepositoryType } from 'xo-remote-parser'
|
||||
@@ -25,10 +26,10 @@ describe('validate', () => {
|
||||
const result = mountAzureForm()
|
||||
|
||||
expect(await result.validate()).toBe(false)
|
||||
expect(result.bindings.hostName.error).toBeDefined()
|
||||
expect(result.bindings.accountName.error).toBeDefined()
|
||||
expect(result.bindings.key.error).toBeDefined()
|
||||
expect(result.bindings.containerName.error).toBeDefined()
|
||||
expect(result.bindings.hostName.error).toEqual(requiredMessage())
|
||||
expect(result.bindings.accountName.error).toEqual(requiredMessage())
|
||||
expect(result.bindings.key.error).toEqual(requiredMessage())
|
||||
expect(result.bindings.containerName.error).toEqual(requiredMessage())
|
||||
})
|
||||
|
||||
it('accepts the required fields without a path in the container', async () => {
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { useLocalBackupRepositoryDetailsForm } from '@/modules/backup-repository/form/details/use-local-backup-repository-details-form.ts'
|
||||
import type { FrontXoProxy } from '@/modules/proxy/remote-resources/use-xo-proxy-collection.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { requiredMessage } from '@/test/form-messages.ts'
|
||||
import { mountComposableWithI18n } from '@/test/mount-composable.ts'
|
||||
import { type MaybeRefOrGetter, nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const PROXY_ID = 'proxy-1' as FrontXoProxy['id']
|
||||
|
||||
function mountLocalForm(proxy?: MaybeRefOrGetter<FrontXoProxy['id'] | undefined>) {
|
||||
return mountComposable(() => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return { ...useLocalBackupRepositoryDetailsForm(proxy), t }
|
||||
}).wrapper.vm
|
||||
return mountComposableWithI18n(() => useLocalBackupRepositoryDetailsForm(proxy))
|
||||
}
|
||||
|
||||
describe('bindings', () => {
|
||||
@@ -41,7 +37,7 @@ describe('validate', () => {
|
||||
const result = mountLocalForm()
|
||||
|
||||
expect(await result.validate()).toBe(false)
|
||||
expect(result.bindings.path.error).toBeDefined()
|
||||
expect(result.bindings.path.error).toEqual(requiredMessage())
|
||||
})
|
||||
|
||||
it('accepts a filled path', async () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useNfsBackupRepositoryDetailsForm } from '@/modules/backup-repository/form/details/use-nfs-backup-repository-details-form.ts'
|
||||
import { NFS_DEFAULT_PORT } from '@/modules/backup-repository/utils/xo-backup-repository-details-payload.util.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { dangerMessage, requiredMessage } from '@/test/form-messages.ts'
|
||||
import { mountComposableWithI18n } from '@/test/mount-composable.ts'
|
||||
import { nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
type NfsFormData = ReturnType<typeof useNfsBackupRepositoryDetailsForm>['formData']
|
||||
|
||||
@@ -12,11 +12,7 @@ const VALID_FORM_DATA = {
|
||||
} satisfies Partial<NfsFormData>
|
||||
|
||||
function mountNfsForm(formData: Partial<NfsFormData> = {}) {
|
||||
const result = mountComposable(() => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return { ...useNfsBackupRepositoryDetailsForm(), t }
|
||||
}).wrapper.vm
|
||||
const result = mountComposableWithI18n(() => useNfsBackupRepositoryDetailsForm())
|
||||
|
||||
Object.assign(result.formData, formData)
|
||||
|
||||
@@ -28,8 +24,8 @@ describe('validate', () => {
|
||||
const result = mountNfsForm()
|
||||
|
||||
expect(await result.validate()).toBe(false)
|
||||
expect(result.bindings.host.error).toBeDefined()
|
||||
expect(result.bindings.path.error).toBeDefined()
|
||||
expect(result.bindings.host.error).toEqual(requiredMessage())
|
||||
expect(result.bindings.path.error).toEqual(requiredMessage())
|
||||
})
|
||||
|
||||
it('accepts a host and a path without a port', async () => {
|
||||
@@ -42,7 +38,7 @@ describe('validate', () => {
|
||||
const result = mountNfsForm({ ...VALID_FORM_DATA, port })
|
||||
|
||||
expect(await result.validate()).toBe(false)
|
||||
expect(result.bindings.port.error).toMatchObject({ content: result.t('invalid-port') })
|
||||
expect(result.bindings.port.error).toEqual(dangerMessage(result.t('invalid-port')))
|
||||
})
|
||||
|
||||
it.each(['1', '2049', '65535'])('accepts the valid port %s', async port => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useS3BackupRepositoryDetailsForm } from '@/modules/backup-repository/form/details/use-s3-backup-repository-details-form.ts'
|
||||
import { requiredMessage } from '@/test/form-messages.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
@@ -37,11 +38,11 @@ describe('validate', () => {
|
||||
const result = mountS3Form()
|
||||
|
||||
expect(await result.validate()).toBe(false)
|
||||
expect(result.bindings.endpoint.error).toBeDefined()
|
||||
expect(result.bindings.region.error).toBeDefined()
|
||||
expect(result.bindings.accessKeyId.error).toBeDefined()
|
||||
expect(result.bindings.secret.error).toBeDefined()
|
||||
expect(result.bindings.bucket.error).toBeDefined()
|
||||
expect(result.bindings.endpoint.error).toEqual(requiredMessage())
|
||||
expect(result.bindings.region.error).toEqual(requiredMessage())
|
||||
expect(result.bindings.accessKeyId.error).toEqual(requiredMessage())
|
||||
expect(result.bindings.secret.error).toEqual(requiredMessage())
|
||||
expect(result.bindings.bucket.error).toEqual(requiredMessage())
|
||||
})
|
||||
|
||||
it('accepts the required fields without a path in the bucket', async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useSmbBackupRepositoryDetailsForm } from '@/modules/backup-repository/form/details/use-smb-backup-repository-details-form.ts'
|
||||
import { SMB_DEFAULT_DOMAIN } from '@/modules/backup-repository/utils/xo-backup-repository-details-payload.util.ts'
|
||||
import { requiredMessage } from '@/test/form-messages.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
@@ -24,9 +25,9 @@ describe('validate', () => {
|
||||
const result = mountSmbForm()
|
||||
|
||||
expect(await result.validate()).toBe(false)
|
||||
expect(result.bindings.pathOnShare.error).toBeDefined()
|
||||
expect(result.bindings.username.error).toBeDefined()
|
||||
expect(result.bindings.password.error).toBeDefined()
|
||||
expect(result.bindings.pathOnShare.error).toEqual(requiredMessage())
|
||||
expect(result.bindings.username.error).toEqual(requiredMessage())
|
||||
expect(result.bindings.password.error).toEqual(requiredMessage())
|
||||
})
|
||||
|
||||
it('accepts a share with credentials, without subfolder, domain or custom options', async () => {
|
||||
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
SMB_DEFAULT_DOMAIN,
|
||||
} from '@/modules/backup-repository/utils/xo-backup-repository-details-payload.util.ts'
|
||||
import type { FrontXoProxy, useXoProxyCollection } from '@/modules/proxy/remote-resources/use-xo-proxy-collection.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { dangerMessage } from '@/test/form-messages.ts'
|
||||
import { mountComposableWithI18n } from '@/test/mount-composable.ts'
|
||||
import { flushPromises } from '@vue/test-utils'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { type BackupRepositoryType, format as formatBackupRepositoryUrl } from 'xo-remote-parser'
|
||||
|
||||
const PROXY_ID = 'proxy-1' as FrontXoProxy['id']
|
||||
@@ -21,11 +21,7 @@ vi.mock(import('@/modules/proxy/remote-resources/use-xo-proxy-collection.ts'), (
|
||||
}))
|
||||
|
||||
function mountNewForm() {
|
||||
return mountComposable(() => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return { ...useNewBackupRepositoryForm(), t }
|
||||
}).wrapper.vm
|
||||
return mountComposableWithI18n(() => useNewBackupRepositoryForm())
|
||||
}
|
||||
|
||||
type NewForm = ReturnType<typeof mountNewForm>
|
||||
@@ -113,9 +109,9 @@ describe('general', () => {
|
||||
await fillEncryptedGeneralStep(form, encryptionKey)
|
||||
|
||||
expect(await form.general.validate()).toBe(false)
|
||||
expect(form.general.bindings.encryptionKey.error).toMatchObject({
|
||||
content: form.t('encryption-key-invalid', { n: 32 }),
|
||||
})
|
||||
expect(form.general.bindings.encryptionKey.error).toEqual(
|
||||
dangerMessage(form.t('encryption-key-invalid', { n: 32 }))
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ import {
|
||||
} from '@/modules/backup-repository/form/use-backup-repository-general-form.ts'
|
||||
import type { useXoProxyCollection } from '@/modules/proxy/remote-resources/use-xo-proxy-collection.ts'
|
||||
import { OBFUSCATED_VALUE } from '@/shared/constants.ts'
|
||||
import { requiredMessage } from '@/test/form-messages.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import UiInput from '@core/components/ui/input/UiInput.vue'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { defineComponent, h, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { BackupRepositoryType } from 'xo-remote-parser'
|
||||
|
||||
vi.mock(import('@/modules/proxy/remote-resources/use-xo-proxy-collection.ts'), () => ({
|
||||
@@ -26,11 +26,7 @@ function createGeneralFormData(): BackupRepositoryGeneralFormData {
|
||||
}
|
||||
|
||||
async function mountGeneralForm(formData: Partial<BackupRepositoryGeneralFormData> = {}) {
|
||||
const result = mountComposable(() => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return { ...useBackupRepositoryGeneralForm(reactive(createGeneralFormData())), t }
|
||||
}).wrapper.vm
|
||||
const result = mountComposable(() => useBackupRepositoryGeneralForm(reactive(createGeneralFormData()))).wrapper.vm
|
||||
|
||||
const { type, ...otherFormData } = formData
|
||||
|
||||
@@ -156,9 +152,9 @@ describe('validate', () => {
|
||||
const result = await mountGeneralForm()
|
||||
|
||||
expect(await result.validate()).toBe(false)
|
||||
expect(result.bindings.name.error).toBeDefined()
|
||||
expect(result.bindings.type.error).toBeDefined()
|
||||
expect(result.bindings.backupFormat.error).toBeDefined()
|
||||
expect(result.bindings.name.error).toEqual(requiredMessage())
|
||||
expect(result.bindings.type.error).toEqual(requiredMessage())
|
||||
expect(result.bindings.backupFormat.error).toEqual(requiredMessage())
|
||||
})
|
||||
|
||||
it('accepts a name, a type and a format, without proxy nor encryption', async () => {
|
||||
@@ -171,7 +167,7 @@ describe('validate', () => {
|
||||
const result = await mountGeneralForm({ name: 'My repository', type: 's3', encrypted: true })
|
||||
|
||||
expect(await result.validate()).toBe(false)
|
||||
expect(result.bindings.encryptionKey.error).toBeDefined()
|
||||
expect(result.bindings.encryptionKey.error).toEqual(requiredMessage())
|
||||
})
|
||||
|
||||
it('accepts any encryption key, its format being checked by the form using it', async () => {
|
||||
@@ -263,7 +259,7 @@ describe('multi-edit mode', () => {
|
||||
}
|
||||
|
||||
// The selects display the label of their selected option
|
||||
function findSelectedLabels(
|
||||
function findShownSelections(
|
||||
formData: Partial<BackupRepositoryGeneralFormData>,
|
||||
mixedFields?: (keyof BackupRepositoryGeneralFormData)[]
|
||||
) {
|
||||
@@ -284,7 +280,7 @@ describe('multi-edit mode', () => {
|
||||
}
|
||||
|
||||
it('shows the mixed value on the type, the format and the proxy which differ between the repositories', () => {
|
||||
expect(findSelectedLabels({ name: 'My repository' }, ['type', 'backupFormat', 'proxy'])).toEqual({
|
||||
expect(findShownSelections({ name: 'My repository' }, ['type', 'backupFormat', 'proxy'])).toEqual({
|
||||
[t('type')]: t('mixed-value'),
|
||||
[t('backup-format')]: t('mixed-value'),
|
||||
[t('proxy')]: t('mixed-value'),
|
||||
@@ -292,7 +288,7 @@ describe('multi-edit mode', () => {
|
||||
})
|
||||
|
||||
it('shows the values shared by all the repositories', () => {
|
||||
const selectedLabels = findSelectedLabels({
|
||||
const selectedLabels = findShownSelections({
|
||||
name: 'My repository',
|
||||
type: 'nfs',
|
||||
backupFormat: 'block',
|
||||
@@ -304,7 +300,7 @@ describe('multi-edit mode', () => {
|
||||
})
|
||||
|
||||
it('removes the proxy of all the repositories with "None"', () => {
|
||||
expect(findSelectedLabels({ name: 'My repository', proxy: null }, ['proxy'])[t('proxy')]).toBe(t('none'))
|
||||
expect(findShownSelections({ name: 'My repository', proxy: null }, ['proxy'])[t('proxy')]).toBe(t('none'))
|
||||
})
|
||||
|
||||
it('accepts a type and a format left mixed', async () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { fetchPost } from '@/shared/utils/fetch.util.ts'
|
||||
import { createBr } from '@/test/create-br.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { silenceConsoleError } from '@/test/silence-console-error.ts'
|
||||
|
||||
const { post, monitorTask } = vi.hoisted(() => ({
|
||||
post: vi.fn(),
|
||||
@@ -51,7 +52,7 @@ describe('run', () => {
|
||||
})
|
||||
|
||||
it('reports each repository result, without stopping at the first failure', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
silenceConsoleError()
|
||||
const error = new Error('Backup repository unreachable')
|
||||
monitorTask.mockRejectedValueOnce(error)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { fetchPatch } from '@/shared/utils/fetch.util.ts'
|
||||
import { createBr } from '@/test/create-br.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { silenceConsoleError } from '@/test/silence-console-error.ts'
|
||||
|
||||
const { patch, checkHealth } = vi.hoisted(() => ({
|
||||
patch: vi.fn(),
|
||||
@@ -65,7 +66,7 @@ describe('run', () => {
|
||||
})
|
||||
|
||||
it('reports each repository result, without stopping at the first failure', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
silenceConsoleError()
|
||||
const error = new Error('Backup repository unreachable')
|
||||
patch.mockRejectedValueOnce(error)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { fetchPatch } from '@/shared/utils/fetch.util.ts'
|
||||
import { createBr } from '@/test/create-br.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { silenceConsoleError } from '@/test/silence-console-error.ts'
|
||||
|
||||
const { patch, checkHealth } = vi.hoisted(() => ({
|
||||
patch: vi.fn(),
|
||||
@@ -62,7 +63,7 @@ describe('run', () => {
|
||||
})
|
||||
|
||||
it('reports each repository result, without stopping at the first failure', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
silenceConsoleError()
|
||||
const error = new Error('Backup repository unreachable')
|
||||
patch.mockRejectedValueOnce(error)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { fetchPost } from '@/shared/utils/fetch.util.ts'
|
||||
import { createBr } from '@/test/create-br.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { silenceConsoleError } from '@/test/silence-console-error.ts'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const { post, monitorTask, backupJobs } = vi.hoisted(() => ({
|
||||
@@ -68,7 +69,7 @@ describe('run', () => {
|
||||
})
|
||||
|
||||
it('reports each repository result, without stopping at the first failure', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
silenceConsoleError()
|
||||
const error = new Error('Backup repository unreachable')
|
||||
monitorTask.mockRejectedValueOnce(error)
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import type { XoHostState } from '@/modules/host/utils/xo-host.util.ts'
|
||||
import type { FrontXoVm, useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
|
||||
import { createHost } from '@/test/create-host.ts'
|
||||
import { createVm } from '@/test/create-vm.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { mountComposable, mountComposableWithI18n } from '@/test/mount-composable.ts'
|
||||
import { getRelativeTime } from '@core/composables/relative-time.composable.ts'
|
||||
import { HOST_ALLOWED_OPERATIONS, HOST_POWER_STATE, VM_OPERATIONS } from '@vates/types'
|
||||
import { ref } from 'vue'
|
||||
import { type MaybeRefOrGetter, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { vmsByHost } = vi.hoisted(() => ({
|
||||
@@ -26,12 +26,12 @@ function setResidentVms(hostId: FrontXoHost['id'], vms: FrontXoVm[]) {
|
||||
vmsByHost.value = new Map([[hostId, vms]])
|
||||
}
|
||||
|
||||
function mountHostUtils(overrides: Partial<FrontXoHost> = {}) {
|
||||
return mountComposable(() => {
|
||||
const { t } = useI18n()
|
||||
function mountHostUtilsFromSource(host: MaybeRefOrGetter<FrontXoHost>) {
|
||||
return mountComposableWithI18n(() => useXoHostUtils(host))
|
||||
}
|
||||
|
||||
return { ...useXoHostUtils(createHost(overrides)), t }
|
||||
}).wrapper.vm
|
||||
function mountHostUtils(overrides: Partial<FrontXoHost> = {}) {
|
||||
return mountHostUtilsFromSource(createHost(overrides))
|
||||
}
|
||||
|
||||
describe('getHostStateDisplay', () => {
|
||||
@@ -139,9 +139,9 @@ describe('isChangingState', () => {
|
||||
|
||||
setResidentVms(host.id, [createVm({ current_operations: { 'task-1': VM_OPERATIONS.SUSPEND } })])
|
||||
|
||||
const { wrapper } = mountComposable(() => useXoHostUtils(host))
|
||||
const result = mountHostUtilsFromSource(host)
|
||||
|
||||
expect(wrapper.vm.isChangingState).toBe(true)
|
||||
expect(result.isChangingState).toBe(true)
|
||||
})
|
||||
|
||||
it('is false when a resident VM is suspending but the host is still enabled', () => {
|
||||
@@ -149,20 +149,20 @@ describe('isChangingState', () => {
|
||||
|
||||
setResidentVms(host.id, [createVm({ current_operations: { 'task-1': VM_OPERATIONS.SUSPEND } })])
|
||||
|
||||
const { wrapper } = mountComposable(() => useXoHostUtils(host))
|
||||
const result = mountHostUtilsFromSource(host)
|
||||
|
||||
expect(wrapper.vm.isChangingState).toBe(false)
|
||||
expect(result.isChangingState).toBe(false)
|
||||
})
|
||||
|
||||
it('reacts to changes of the source host', () => {
|
||||
const host = ref(createHost({ current_operations: {} }))
|
||||
const { wrapper } = mountComposable(() => useXoHostUtils(host))
|
||||
const result = mountHostUtilsFromSource(host)
|
||||
|
||||
expect(wrapper.vm.isChangingState).toBe(false)
|
||||
expect(result.isChangingState).toBe(false)
|
||||
|
||||
host.value = createHost({ current_operations: { 'task-1': HOST_ALLOWED_OPERATIONS.REBOOT } })
|
||||
|
||||
expect(wrapper.vm.isChangingState).toBe(true)
|
||||
expect(result.isChangingState).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -189,13 +189,9 @@ describe('currentOperation', () => {
|
||||
|
||||
setResidentVms(host.id, [createVm({ current_operations: { 'task-1': VM_OPERATIONS.HARD_SHUTDOWN } })])
|
||||
|
||||
const { wrapper } = mountComposable(() => {
|
||||
const { t } = useI18n()
|
||||
const result = mountHostUtilsFromSource(host)
|
||||
|
||||
return { ...useXoHostUtils(host), t }
|
||||
})
|
||||
|
||||
expect(wrapper.vm.currentOperation).toBe(wrapper.vm.t('operation:vm-hard-shutdown'))
|
||||
expect(result.currentOperation).toBe(result.t('operation:vm-hard-shutdown'))
|
||||
})
|
||||
|
||||
it('falls back to an empty label when there is no pending operation', () => {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<NetworkFormTextInput v-bind="nameInputBindings" />
|
||||
<NetworkFormNumberInput v-bind="mtuInputBindings" />
|
||||
<VtsNumberInput v-bind="mtuInputBindings" />
|
||||
</div>
|
||||
<NewNetworkDescriptionTextarea v-bind="descriptionInputBindings" />
|
||||
</div>
|
||||
@@ -20,7 +20,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import NetworkFormNumberInput from '@/modules/network/components/form/new/inputs/NetworkFormNumberInput.vue'
|
||||
import NetworkFormSelect from '@/modules/network/components/form/new/inputs/NetworkFormSelect.vue'
|
||||
import NetworkFormTextInput from '@/modules/network/components/form/new/inputs/NetworkFormTextInput.vue'
|
||||
import NewNetworkDescriptionTextarea from '@/modules/network/components/form/new/inputs/NewNetworkDescriptionTextarea.vue'
|
||||
@@ -29,6 +28,7 @@ import NewNetworkButtonsSection from '@/modules/network/components/form/new/NewN
|
||||
import { useNewBondedNetworkForm } from '@/modules/network/form/new-bonded/use-new-bonded-network-form.ts'
|
||||
import type { NewBondedNetworkPayload } from '@/modules/network/jobs/xo-bonded-network-create.job.ts'
|
||||
import type { FrontXoPool } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
|
||||
import VtsNumberInput from '@core/components/number-input/VtsNumberInput.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<NetworkFormTextInput v-bind="nameInputBindings" />
|
||||
<NetworkFormNumberInput v-bind="mtuInputBindings" />
|
||||
<VtsNumberInput v-bind="mtuInputBindings" />
|
||||
</div>
|
||||
<NewNetworkDescriptionTextarea v-bind="descriptionInputBindings" />
|
||||
</div>
|
||||
@@ -16,7 +16,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import NetworkFormNumberInput from '@/modules/network/components/form/new/inputs/NetworkFormNumberInput.vue'
|
||||
import NetworkFormSelect from '@/modules/network/components/form/new/inputs/NetworkFormSelect.vue'
|
||||
import NetworkFormTextInput from '@/modules/network/components/form/new/inputs/NetworkFormTextInput.vue'
|
||||
import NewNetworkDescriptionTextarea from '@/modules/network/components/form/new/inputs/NewNetworkDescriptionTextarea.vue'
|
||||
@@ -25,6 +24,7 @@ import NewNetworkButtonsSection from '@/modules/network/components/form/new/NewN
|
||||
import { useNewInternalNetworkForm } from '@/modules/network/form/new-internal/use-new-internal-network-form.ts'
|
||||
import type { NewInternalNetworkPayload } from '@/modules/network/jobs/xo-internal-network-create.job.ts'
|
||||
import type { FrontXoPool } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
|
||||
import VtsNumberInput from '@core/components/number-input/VtsNumberInput.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<NetworkFormTextInput v-bind="nameInputBindings" />
|
||||
<NetworkFormNumberInput v-bind="mtuInputBindings" />
|
||||
<VtsNumberInput v-bind="mtuInputBindings" />
|
||||
</div>
|
||||
<NewNetworkDescriptionTextarea v-bind="descriptionInputBindings" />
|
||||
</div>
|
||||
<NetworkFormNumberInput v-bind="vlanInputBindings" class="vlan" />
|
||||
<VtsNumberInput v-bind="vlanInputBindings" class="vlan" />
|
||||
<div class="nbd">
|
||||
<NewNetworkNbdRadio v-bind="nbdRadioBindings" />
|
||||
</div>
|
||||
@@ -20,7 +20,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import NetworkFormNumberInput from '@/modules/network/components/form/new/inputs/NetworkFormNumberInput.vue'
|
||||
import NetworkFormSelect from '@/modules/network/components/form/new/inputs/NetworkFormSelect.vue'
|
||||
import NetworkFormTextInput from '@/modules/network/components/form/new/inputs/NetworkFormTextInput.vue'
|
||||
import NewNetworkDescriptionTextarea from '@/modules/network/components/form/new/inputs/NewNetworkDescriptionTextarea.vue'
|
||||
@@ -29,6 +28,7 @@ import NewNetworkButtonsSection from '@/modules/network/components/form/new/NewN
|
||||
import { useNewNetworkForm } from '@/modules/network/form/new/use-new-network-form.ts'
|
||||
import type { NewNetworkPayload } from '@/modules/network/jobs/xo-network-create.job.ts'
|
||||
import type { FrontXoPool } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
|
||||
import VtsNumberInput from '@core/components/number-input/VtsNumberInput.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<VtsInputWrapper :label :message="messages">
|
||||
<UiInput v-model.number="model" accent="brand" type="number" :required @blur="emit('blur')" />
|
||||
</VtsInputWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { InputWrapperMessage } from '@core/components/input-wrapper/VtsInputWrapper.vue'
|
||||
import VtsInputWrapper from '@core/components/input-wrapper/VtsInputWrapper.vue'
|
||||
import UiInput from '@core/components/ui/input/UiInput.vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { info, warning, error } = defineProps<{
|
||||
label: string
|
||||
error?: InputWrapperMessage
|
||||
warning?: InputWrapperMessage
|
||||
info?: string
|
||||
required?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ blur: [] }>()
|
||||
const model = defineModel<number | undefined>()
|
||||
|
||||
const messages = computed<InputWrapperMessage>(
|
||||
() => [info, warning, error].filter(message => message !== undefined) as InputWrapperMessage
|
||||
)
|
||||
</script>
|
||||
@@ -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,8 +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 type { useOverlay } from '@core/packages/overlay/use-overlay.ts'
|
||||
import { silenceConsoleError } from '@/test/silence-console-error.ts'
|
||||
import { KEEP_OVERLAY_OPEN } from '@core/packages/overlay/symbols.ts'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
@@ -16,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()
|
||||
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
silenceConsoleError()
|
||||
|
||||
useJob.mockReturnValue(createJobResult(run))
|
||||
useOverlayMock.mockReturnValue({ open, abort: vi.fn() })
|
||||
})
|
||||
|
||||
function mountDrawerJob(plugin: FrontXoPlugin = createPlugin()) {
|
||||
@@ -49,7 +44,7 @@ function mountDrawerJob(plugin: FrontXoPlugin = createPlugin()) {
|
||||
}
|
||||
|
||||
function getOverlayOptions(): OverlayOptions {
|
||||
return useOverlayMock.mock.calls[0][0]
|
||||
return overlayMock.getOverlay<OverlayOptions>()
|
||||
}
|
||||
|
||||
function getJobArgs() {
|
||||
@@ -86,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', () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { FrontXoPlugin } from '@/modules/plugin/remote-resources/use-xo-plu
|
||||
import type { fetchPatch } from '@/shared/utils/fetch.util.ts'
|
||||
import { createPlugin } from '@/test/create-plugin.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { expectJobRunning } from '@/test/job-running.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
|
||||
const { patch } = vi.hoisted(() => ({
|
||||
@@ -63,9 +64,6 @@ describe('validate', () => {
|
||||
patch.mockReturnValue(new Promise(() => {}))
|
||||
const job = mountAutoloadJob(createPlugin())
|
||||
|
||||
void job.run()
|
||||
|
||||
expect(job.isRunning).toBe(true)
|
||||
expect(job.errorMessage).toBe(t('job:edit:in-progress'))
|
||||
expectJobRunning(job, t('job:edit:in-progress'))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,7 +4,9 @@ import type { useXoTaskUtils } from '@/shared/composables/xo-task-utils.composab
|
||||
import type { fetchPatch, fetchPost } from '@/shared/utils/fetch.util.ts'
|
||||
import { createPlugin } from '@/test/create-plugin.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { expectJobRunning } from '@/test/job-running.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { silenceConsoleError } from '@/test/silence-console-error.ts'
|
||||
|
||||
const { post, patch, monitorTask } = vi.hoisted(() => ({
|
||||
post: vi.fn(),
|
||||
@@ -54,7 +56,7 @@ describe('run', () => {
|
||||
})
|
||||
|
||||
it('succeeds when only the autoload update fails', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
silenceConsoleError()
|
||||
patch.mockRejectedValue(new Error('autoload update failed'))
|
||||
|
||||
await expect(mountChangeStateJob(unloadedPlugin, true).run()).resolves.toBeUndefined()
|
||||
@@ -127,9 +129,6 @@ describe('validate', () => {
|
||||
post.mockReturnValue(new Promise(() => {}))
|
||||
const job = mountChangeStateJob(plugin, loaded)
|
||||
|
||||
void job.run()
|
||||
|
||||
expect(job.isRunning).toBe(true)
|
||||
expect(job.errorMessage).toBe(t(key))
|
||||
expectJobRunning(job, t(key))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { FrontXoPlugin } from '@/modules/plugin/remote-resources/use-xo-plu
|
||||
import type { fetchPut } from '@/shared/utils/fetch.util.ts'
|
||||
import { createPlugin } from '@/test/create-plugin.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { expectJobRunning } from '@/test/job-running.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
|
||||
const { put } = vi.hoisted(() => ({
|
||||
@@ -72,9 +73,6 @@ describe('validate', () => {
|
||||
put.mockReturnValue(new Promise(() => {}))
|
||||
const job = mountConfigureJob(configurablePlugin)
|
||||
|
||||
void job.run()
|
||||
|
||||
expect(job.isRunning).toBe(true)
|
||||
expect(job.errorMessage).toBe(t('job:edit:in-progress'))
|
||||
expectJobRunning(job, t('job:edit:in-progress'))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { FrontXoPlugin } from '@/modules/plugin/remote-resources/use-xo-plu
|
||||
import type { fetchDelete } from '@/shared/utils/fetch.util.ts'
|
||||
import { createPlugin } from '@/test/create-plugin.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { expectJobRunning } from '@/test/job-running.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
|
||||
const { remove } = vi.hoisted(() => ({
|
||||
@@ -79,9 +80,6 @@ describe('validate', () => {
|
||||
remove.mockReturnValue(new Promise(() => {}))
|
||||
const job = mountDeleteConfigurationJob(configuredPlugin)
|
||||
|
||||
void job.run()
|
||||
|
||||
expect(job.isRunning).toBe(true)
|
||||
expect(job.errorMessage).toBe(t('job:delete:in-progress'))
|
||||
expectJobRunning(job, t('job:delete:in-progress'))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { useXoTaskUtils } from '@/shared/composables/xo-task-utils.composab
|
||||
import type { fetchPost } from '@/shared/utils/fetch.util.ts'
|
||||
import { createPlugin } from '@/test/create-plugin.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { expectJobRunning } from '@/test/job-running.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
|
||||
const { post, monitorTask } = vi.hoisted(() => ({
|
||||
@@ -85,9 +86,6 @@ describe('validate', () => {
|
||||
post.mockReturnValue(new Promise(() => {}))
|
||||
const job = mountTestJob(testablePlugin)
|
||||
|
||||
void job.run()
|
||||
|
||||
expect(job.isRunning).toBe(true)
|
||||
expect(job.errorMessage).toBe(t('job:plugin-test:in-progress'))
|
||||
expectJobRunning(job, t('job:plugin-test:in-progress'))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
<div class="primary-host-section">
|
||||
<UiTitle>{{ t('master') }}</UiTitle>
|
||||
<div class="inputs-container">
|
||||
<PoolConnectionFormTextInput v-bind="hostInputBindings" />
|
||||
<PoolConnectionFormTextInput v-bind="httpProxyInputBindings" />
|
||||
<PoolConnectionFormTextInput v-bind="usernameInputBindings" />
|
||||
<PoolConnectionFormPasswordInput v-bind="passwordInputBindings" />
|
||||
<FormTextInput v-bind="hostInputBindings" />
|
||||
<FormTextInput v-bind="httpProxyInputBindings" />
|
||||
<FormTextInput v-bind="usernameInputBindings" />
|
||||
<FormPasswordInput v-bind="passwordInputBindings" />
|
||||
</div>
|
||||
</div>
|
||||
<UiTitle>{{ t('options') }}</UiTitle>
|
||||
<div class="options-section">
|
||||
<PoolConnectionFormCheckbox v-bind="readOnlyCheckboxBindings" />
|
||||
<PoolConnectionFormCheckbox v-bind="allowUnauthorizedCheckboxBindings" />
|
||||
<FormCheckbox v-bind="readOnlyCheckboxBindings" />
|
||||
<FormCheckbox v-bind="allowUnauthorizedCheckboxBindings" />
|
||||
</div>
|
||||
<div class="buttons-container">
|
||||
<UiLink :to="{ name: '/(site)/dashboard' }" size="medium">
|
||||
@@ -26,14 +26,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PoolConnectionFormCheckbox from '@/modules/pool/components/connection/inputs/PoolConnectionFormCheckbox.vue'
|
||||
import PoolConnectionFormPasswordInput from '@/modules/pool/components/connection/inputs/PoolConnectionFormPasswordInput.vue'
|
||||
import PoolConnectionFormTextInput from '@/modules/pool/components/connection/inputs/PoolConnectionFormTextInput.vue'
|
||||
import { usePoolConnectionForm } from '@/modules/pool/form/use-pool-connection-form.ts'
|
||||
import { isAuthenticationFailedError } from '@/modules/pool/utils/xo-pool.util.ts'
|
||||
import { useXoServerConnectJob } from '@/modules/server/jobs/xo-server-connect.job.ts'
|
||||
import { useXoServerCreateJob } from '@/modules/server/jobs/xo-server-create.job.ts'
|
||||
import { useXoServerForgetJob } from '@/modules/server/jobs/xo-server-forget.job.ts'
|
||||
import FormCheckbox from '@/shared/components/form/FormCheckbox.vue'
|
||||
import FormPasswordInput from '@/shared/components/form/FormPasswordInput.vue'
|
||||
import FormTextInput from '@/shared/components/form/FormTextInput.vue'
|
||||
import VtsForm from '@core/components/form/VtsForm.vue'
|
||||
import UiButton from '@core/components/ui/button/UiButton.vue'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import PoolConnectionSuccess from '@/modules/pool/components/connection/PoolConnectionSuccess.vue'
|
||||
import type {
|
||||
FrontXoServer,
|
||||
useXoServerCollection,
|
||||
} from '@/modules/server/remote-resources/use-xo-server-collection.ts'
|
||||
import { createServer } from '@/test/create-server.ts'
|
||||
import { findTitle } from '@/test/find-titles.ts'
|
||||
import { getButton } from '@/test/get-button.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import UiAlert from '@core/components/ui/alert/UiAlert.vue'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { computed, ref, toValue, type MaybeRefOrGetter } from 'vue'
|
||||
|
||||
const servers = ref<FrontXoServer[]>([])
|
||||
|
||||
vi.mock(import('@/modules/server/remote-resources/use-xo-server-collection.ts'), () => ({
|
||||
useXoServerCollection: (() => ({
|
||||
useGetServerById: (id: MaybeRefOrGetter<FrontXoServer['id']>) =>
|
||||
computed(() => servers.value.find(server => server.id === toValue(id))),
|
||||
})) as unknown as typeof useXoServerCollection,
|
||||
}))
|
||||
|
||||
const SERVER = createServer({ host: '10.0.0.1' })
|
||||
|
||||
function mountSuccess() {
|
||||
return mount(PoolConnectionSuccess, { props: { serverId: SERVER.id }, global: createGlobalTestConfig() })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
servers.value = [SERVER]
|
||||
})
|
||||
|
||||
it('tells the IP address the pool is connected to', () => {
|
||||
expect(findTitle(mountSuccess())).toBe(t('connected-to-ip', { ip: '10.0.0.1' }))
|
||||
})
|
||||
|
||||
it('tells the connection succeeded', () => {
|
||||
const alert = mountSuccess().getComponent(UiAlert)
|
||||
|
||||
expect(alert.props('accent')).toBe('success')
|
||||
expect(alert.text()).toBe(t('pool-connection-success'))
|
||||
})
|
||||
|
||||
it('links to the dashboard of the connected pool', () => {
|
||||
const link = mountSuccess().getComponent(UiLink)
|
||||
|
||||
expect(link.text()).toBe(t('action:visit-pool-dashboard'))
|
||||
expect(link.props('to')).toEqual({ name: '/pool/[id]/dashboard', params: { id: SERVER.poolId } })
|
||||
})
|
||||
|
||||
it('offers to connect another pool', async () => {
|
||||
const wrapper = mountSuccess()
|
||||
|
||||
await getButton(wrapper, t('action:connect-another-pool')).trigger('click')
|
||||
|
||||
expect(wrapper.emitted('connectAnotherPool')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('has neither title nor link while the server is unknown', () => {
|
||||
servers.value = []
|
||||
|
||||
const wrapper = mountSuccess()
|
||||
|
||||
expect(findTitle(wrapper)).toBeUndefined()
|
||||
expect(wrapper.findComponent(UiLink).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('has no link while the pool of the server is unknown', () => {
|
||||
servers.value = [{ ...SERVER, poolId: undefined }]
|
||||
|
||||
expect(mountSuccess().findComponent(UiLink).exists()).toBe(false)
|
||||
})
|
||||
@@ -1,46 +1,40 @@
|
||||
<template>
|
||||
<VtsStateHero class="connection-success" type="all-good" format="card" size="medium">
|
||||
<div class="container">
|
||||
<div v-if="server" class="typo-h1 title">{{ t('connected-to-ip', { ip: server.host }) }}</div>
|
||||
<div class="content" :class="{ mobile: uiStore.isSmall, desktop: !uiStore.isLarge }">
|
||||
<UiAlert accent="success">
|
||||
{{ t('pool-connection-success') }}
|
||||
</UiAlert>
|
||||
<div class="action-buttons" :class="{ mobile: uiStore.isSmall }">
|
||||
<UiLink
|
||||
v-if="server && server.poolId"
|
||||
:to="{ name: '/pool/[id]/dashboard', params: { id: server.poolId } }"
|
||||
size="medium"
|
||||
>
|
||||
{{ t('action:visit-pool-dashboard') }}
|
||||
</UiLink>
|
||||
<UiButton
|
||||
variant="secondary"
|
||||
accent="brand"
|
||||
size="medium"
|
||||
left-icon="fa:plus"
|
||||
@click="emit('connectAnotherPool')"
|
||||
>
|
||||
{{ t('action:connect-another-pool') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VtsStateHero>
|
||||
<VtsSuccessStateHero>
|
||||
<template v-if="server" #title>{{ t('connected-to-ip', { ip: server.host }) }}</template>
|
||||
{{ t('pool-connection-success') }}
|
||||
<template #actions>
|
||||
<UiLink
|
||||
v-if="server && server.poolId"
|
||||
:to="{ name: '/pool/[id]/dashboard', params: { id: server.poolId } }"
|
||||
size="medium"
|
||||
>
|
||||
{{ t('action:visit-pool-dashboard') }}
|
||||
</UiLink>
|
||||
<UiButton
|
||||
variant="secondary"
|
||||
accent="brand"
|
||||
size="medium"
|
||||
left-icon="fa:plus"
|
||||
@click="emit('connectAnotherPool')"
|
||||
>
|
||||
{{ t('action:connect-another-pool') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
</VtsSuccessStateHero>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { 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 {
|
||||
type FrontXoServer,
|
||||
useXoServerCollection,
|
||||
} from '@/modules/server/remote-resources/use-xo-server-collection.ts'
|
||||
import VtsSuccessStateHero from '@core/components/success-state-hero/VtsSuccessStateHero.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<{
|
||||
@@ -49,49 +43,6 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
const { useGetServerById } = useXoServerCollection()
|
||||
const uiStore = useUiStore()
|
||||
|
||||
const server = useGetServerById(() => serverId)
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.connection-success {
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--color-neutral-txt-primary);
|
||||
}
|
||||
|
||||
.content {
|
||||
color: var(--color-neutral-txt-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rem;
|
||||
margin: 0 auto;
|
||||
width: 65%;
|
||||
|
||||
&.desktop {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2.4rem;
|
||||
|
||||
&.mobile {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<UiCheckbox v-model="model" accent="brand">
|
||||
{{ label }}
|
||||
</UiCheckbox>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiCheckbox from '@core/components/ui/checkbox/UiCheckbox.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
defineProps<{
|
||||
label: string
|
||||
}>()
|
||||
|
||||
const model = defineModel<boolean>({ required: true })
|
||||
</script>
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<VtsInputWrapper :label :message="messages" wrap-message>
|
||||
<UiInput v-model.trim="model" accent="brand" :required :placeholder @blur="emit('blur')" />
|
||||
</VtsInputWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { InputWrapperMessage } from '@core/components/input-wrapper/VtsInputWrapper.vue'
|
||||
import VtsInputWrapper from '@core/components/input-wrapper/VtsInputWrapper.vue'
|
||||
import UiInput from '@core/components/ui/input/UiInput.vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { info, error } = defineProps<{
|
||||
label: string
|
||||
info?: string
|
||||
error?: InputWrapperMessage
|
||||
required?: boolean
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ blur: [] }>()
|
||||
|
||||
const model = defineModel<string>({ required: true })
|
||||
|
||||
const messages = computed<InputWrapperMessage>(
|
||||
() => [info, error].filter(message => message !== undefined) as InputWrapperMessage
|
||||
)
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { InputWrapperMessage } from '@core/components/input-wrapper/VtsInputWrapper.vue'
|
||||
import { useCredentialsError } from '@/shared/composables/credentials-error.composable.ts'
|
||||
import { required, withMessage } from '@core/packages/form-validation'
|
||||
import { useValidatedForm } from '@core/packages/validated-form'
|
||||
import { computed, type ComputedRef, reactive, ref, watch } from 'vue'
|
||||
import { computed, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export type PoolConnectionFormData = {
|
||||
@@ -40,21 +40,10 @@ export function usePoolConnectionForm() {
|
||||
httpProxy: formData.httpProxy === '' ? undefined : formData.httpProxy,
|
||||
}))
|
||||
|
||||
const credentialsError = ref<InputWrapperMessage>()
|
||||
|
||||
watch([() => formData.username, () => formData.password], () => {
|
||||
credentialsError.value = undefined
|
||||
})
|
||||
|
||||
function showCredentialsError() {
|
||||
credentialsError.value = { content: t('invalid-username-or-password'), accent: 'danger' }
|
||||
}
|
||||
|
||||
function withCredentialsError<T extends { error: InputWrapperMessage | undefined }>(
|
||||
field: ComputedRef<T>
|
||||
): ComputedRef<T> {
|
||||
return computed(() => ({ ...field.value, error: field.value.error ?? credentialsError.value }))
|
||||
}
|
||||
const { showCredentialsError, withCredentialsError } = useCredentialsError([
|
||||
() => formData.username,
|
||||
() => formData.password,
|
||||
])
|
||||
|
||||
return {
|
||||
formData,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { usePoolConnectionForm } from '@/modules/pool/form/use-pool-connection-form.ts'
|
||||
import { describeCredentialsErrorWiring } from '@/test/credentials-error-wiring.ts'
|
||||
import { dangerMessage } from '@/test/form-messages.ts'
|
||||
import { mountComposableWithI18n } from '@/test/mount-composable.ts'
|
||||
|
||||
function mountPoolConnectionForm() {
|
||||
return mountComposableWithI18n(() => usePoolConnectionForm())
|
||||
}
|
||||
|
||||
type PoolConnectionForm = ReturnType<typeof mountPoolConnectionForm>
|
||||
|
||||
function fillRequiredFields(form: PoolConnectionForm) {
|
||||
Object.assign(form.formData, { host: '192.168.1.10', username: 'root', password: 'secret' })
|
||||
}
|
||||
|
||||
describe('validate', () => {
|
||||
it('reports each missing required field with its own message', async () => {
|
||||
const form = mountPoolConnectionForm()
|
||||
|
||||
expect(await form.validate()).toBe(false)
|
||||
expect(form.hostInputBindings.error).toEqual(dangerMessage(form.t('ip-address-required')))
|
||||
expect(form.usernameInputBindings.error).toEqual(dangerMessage(form.t('username-required')))
|
||||
expect(form.passwordInputBindings.error).toEqual(dangerMessage(form.t('password-required')))
|
||||
})
|
||||
|
||||
it('accepts a form whose required fields are filled', async () => {
|
||||
const form = mountPoolConnectionForm()
|
||||
fillRequiredFields(form)
|
||||
|
||||
expect(await form.validate()).toBe(true)
|
||||
expect(form.hostInputBindings.error).toBeUndefined()
|
||||
expect(form.usernameInputBindings.error).toBeUndefined()
|
||||
expect(form.passwordInputBindings.error).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('payload', () => {
|
||||
it('carries every field and drops an empty http proxy', () => {
|
||||
const form = mountPoolConnectionForm()
|
||||
fillRequiredFields(form)
|
||||
|
||||
expect(form.payload).toEqual({
|
||||
host: '192.168.1.10',
|
||||
httpProxy: undefined,
|
||||
username: 'root',
|
||||
password: 'secret',
|
||||
readOnly: false,
|
||||
allowUnauthorized: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a filled http proxy', () => {
|
||||
const form = mountPoolConnectionForm()
|
||||
form.formData.httpProxy = 'http://proxy:3128'
|
||||
|
||||
expect(form.payload.httpProxy).toBe('http://proxy:3128')
|
||||
})
|
||||
})
|
||||
|
||||
describeCredentialsErrorWiring(mountPoolConnectionForm, fillRequiredFields)
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
|
||||
import { useXoSrUtils } from '@/modules/storage-repository/composables/xo-sr-utils.composable.ts'
|
||||
import { ONE_GB } from '@/shared/constants.ts'
|
||||
import { createHost } from '@/test/create-host.ts'
|
||||
import { createPbd } from '@/test/create-pbd.ts'
|
||||
import { createSr } from '@/test/create-sr.ts'
|
||||
import { xoCollectionsMock } from '@/test/mock-xo-collections.ts'
|
||||
import { mountComposableWithI18n } from '@/test/mount-composable.ts'
|
||||
|
||||
vi.mock(
|
||||
import('@/modules/host/remote-resources/use-xo-host-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')
|
||||
)
|
||||
|
||||
const HOST = createHost({ name_label: 'Host A' })
|
||||
const LOCAL_SR = createSr({ shared: false })
|
||||
const PBD_ON_UNKNOWN_HOST = createPbd({ SR: LOCAL_SR.id, host: 'unknown-host' as FrontXoHost['id'] })
|
||||
|
||||
beforeEach(() => {
|
||||
xoCollectionsMock.reset()
|
||||
xoCollectionsMock.hosts.value = [HOST]
|
||||
})
|
||||
|
||||
describe('getSrSelectLabel', () => {
|
||||
it.each([
|
||||
['the whole GB left', createSr({ shared: true, size: 10 * ONE_GB, physical_usage: 1.5 * ONE_GB }), 8],
|
||||
[
|
||||
'no GB left once it uses more than its size',
|
||||
createSr({ shared: true, size: 10 * ONE_GB, physical_usage: 20 * ONE_GB }),
|
||||
0,
|
||||
],
|
||||
])('labels the SR with its location and %s', (_, sr, gbLeft) => {
|
||||
const { getSrSelectLabel, t } = mountComposableWithI18n(() => useXoSrUtils())
|
||||
|
||||
expect(getSrSelectLabel(sr)).toBe(`${sr.name_label} (${t('shared')}) - ${t('n-gb-left', { n: gbLeft })}`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSrLocation', () => {
|
||||
it('locates a local SR on the first known host of its PBDs', () => {
|
||||
xoCollectionsMock.pbds.value = [PBD_ON_UNKNOWN_HOST, createPbd({ SR: LOCAL_SR.id, host: HOST.id })]
|
||||
|
||||
const { getSrLocation } = mountComposableWithI18n(() => useXoSrUtils())
|
||||
|
||||
expect(getSrLocation(LOCAL_SR)).toBe(HOST.name_label)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['it has no PBD', []],
|
||||
['the host of its PBD is unknown', [PBD_ON_UNKNOWN_HOST]],
|
||||
])('reads unknown for a local SR when %s', (_, pbds) => {
|
||||
xoCollectionsMock.pbds.value = pbds
|
||||
|
||||
const { getSrLocation, t } = mountComposableWithI18n(() => useXoSrUtils())
|
||||
|
||||
expect(getSrLocation(LOCAL_SR)).toBe(t('unknown'))
|
||||
})
|
||||
})
|
||||
@@ -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]',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user