feat(xo-6/pool-dashboard): refactored duplicated progress bar groups components (#10386)

This commit is contained in:
OlivierFL
2026-10-27 18:14:02 +01:00
committed by GitHub
parent 4cfe9e81bd
commit 2685f3b95c
72 changed files with 415 additions and 703 deletions

View File

@@ -0,0 +1,39 @@
<template>
<VtsStateHero v-if="busy" format="card" type="busy" size="medium" />
<VtsStateHero v-else-if="hasError" format="card" type="error" size="medium">
{{ t('error-no-data') }}
</VtsStateHero>
<VtsStateHero v-else-if="items.length === 0" format="card" type="no-data" size="medium">
{{ t('no-data-to-calculate') }}
</VtsStateHero>
<template v-else>
<VtsProgressBarGroup :items :legend-type :thresholds />
<slot />
</template>
</template>
<script lang="ts" setup>
import type {
ProgressBarLegendType,
ProgressBarThresholdConfig,
} from '@core/components/progress-bar/VtsProgressBar.vue'
import VtsProgressBarGroup, {
type ProgressBarGroupItem,
} from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import { useI18n } from 'vue-i18n'
defineProps<{
items: ProgressBarGroupItem[]
legendType: ProgressBarLegendType
busy?: boolean
hasError?: boolean
thresholds?: ProgressBarThresholdConfig
}>()
defineSlots<{
default?(): any
}>()
const { t } = useI18n()
</script>

View File

@@ -2,20 +2,21 @@
Domain objects are created through factories in `src/test/`, one per object type:
| Factory | Builds |
| ------------------ | ------------------- |
| `createVm` | `FrontXoVm` |
| `createHost` | `FrontXoHost` |
| `createServer` | `FrontXoServer` |
| `createPool` | `FrontXoPool` |
| `createSr` | `FrontXoSr` |
| `createVbd` | `FrontXoVbd` |
| `createVdi` | `FrontXoVdi` |
| `createVmSnapshot` | `FrontXoVmSnapshot` |
| `createPlugin` | `FrontXoPlugin` |
| `createHostStats` | `XapiHostStats` |
| `createVmStats` | `XapiVmStats` |
| `createPoolStats` | `XapiPoolStats` |
| Factory | Builds |
| --------------------------------- | ------------------------------------------ |
| `createVm` | `FrontXoVm` |
| `createHost` | `FrontXoHost` |
| `createServer` | `FrontXoServer` |
| `createPool` | `FrontXoPool` |
| `createSr` | `FrontXoSr` |
| `createVbd` | `FrontXoVbd` |
| `createVdi` | `FrontXoVdi` |
| `createVmSnapshot` | `FrontXoVmSnapshot` |
| `createPlugin` | `FrontXoPlugin` |
| `createHostStats` | `XapiHostStats` |
| `createVmStats` | `XapiVmStats` |
| `createPoolStats` | `XapiPoolStats` |
| `createPoolDashboardTopFiveUsage` | `XoPoolDashboard` (top-five usage section) |
`createPoolStats` composes `createHostStats`, since pool stats are one entry per host: pass a `Partial<XapiHostStats>` per host id, or an `{ error }` entry for a host whose stats the pool could not fetch.
@@ -26,7 +27,13 @@ const poolStats = createPoolStats({
})
```
Shared _helper_ factories live there too — `mount-composable.ts`, `mount-chart-card.ts`, `create-enhanced-data-helpers.ts`, `global-test-config.ts`, `create-test-router.ts`, `create-job-result.ts` (what a mocked `defineJob` composable returns), `create-action-item.ts` (a menu / button `LeafActionItem`), `create-plugin-actions.ts` (what a mocked `useXoPluginActions` returns), `find-labelled-values.ts`, `find-tags.ts` (the labels of the rendered tags), `find-titles.ts` (the card and section titles), `find-tooltip-content.ts` (the tooltip an element shows on hover), `get-button.ts` (a button by its label), `get-checkbox.ts` (the input of a `UiCheckbox` by its label), `drawer.ts` (submit or cancel a drawer, read its buttons), `select.ts` (open a `VtsSelect`, read or pick its options), `mount-json-schema-form.ts` (a JSON schema form, or a composable run in one of its fields), `json-schema-item-label.ts` (the labels of the items of a JSON schema list), `i18n.ts` and `linear-chart-stub.ts`.
`createPoolDashboardTopFiveUsage` builds the `hosts` and `vms` top-five usage sections of an `XoPoolDashboard`. Both metrics of a section default to empty, so a card test fills in only the one it exercises:
```typescript
const dashboard = createPoolDashboardTopFiveUsage({ cpu: [createHostCpuUsage()] }, { cpu: [createVmCpuUsage()] })
```
Shared _helper_ factories live there too — `mount-composable.ts`, `mount-chart-card.ts`, `create-enhanced-data-helpers.ts`, `global-test-config.ts`, `create-test-router.ts`, `create-job-result.ts` (what a mocked `defineJob` composable returns), `create-action-item.ts` (a menu / button `LeafActionItem`), `create-plugin-actions.ts` (what a mocked `useXoPluginActions` returns), `find-rendered-values.ts`, `find-titles.ts` (the card and section titles), `find-tooltip-content.ts` (the tooltip an element shows on hover), `get-button.ts` (a button by its label), `get-checkbox.ts` (the input of a `UiCheckbox` by its label), `drawer.ts` (submit or cancel a drawer, read its buttons), `select.ts` (open a `VtsSelect`, read or pick its options), `mount-json-schema-form.ts` (a JSON schema form, or a composable run in one of its fields), `json-schema-item-label.ts` (the labels of the items of a JSON schema list), `i18n.ts` and `linear-chart-stub.ts`.
Each factory returns a **fully-populated** object of the real front-end type and accepts a `Partial<T>` of overrides, spread last, so a test only states the fields relevant to its case:

View File

@@ -61,7 +61,7 @@ expect(labelledValues).toEqual({ [t('vga')]: t('disabled'), [t('video-ram')]: '8
Prefer this shape: one assertion covering every row a user sees, and it fails loudly when a value lands under the wrong label. Assert against **class names and semantic elements**, never a positional chain of child indexes.
`src/test/find-labelled-values.ts` does exactly that reduction, so a card test does not re-roll it:
`src/test/find-rendered-values.ts` does exactly that reduction, so a card test does not re-roll it:
| Helper | Reads |
| ------------------------ | ---------------------------------------------------------- |
@@ -91,6 +91,22 @@ expect(wrapper.findAll('.vts-card-row-key-value').map(row => row.get('.value').t
When a card mixes deterministic values with environment-derived ones (a relative start time, a formatted date), pin the deterministic rows with `toMatchObject` and cover the composition separately by asserting the **list of labels, in order** — the formatted values themselves belong in the test of the composable that derives them.
Two more readers live in the same file, for output that is not a label/value pair:
| Helper | Reads |
| --------------- | ----------------------------------------------------------------- |
| `findTableRows` | the `tbody` of a table, as one array of **cell** texts per row |
| `findTags` | every `UiTag`, as its text, in the order the component lists them |
```typescript
expect(findTableRows(wrapper)).toEqual([
['XSAPATCH-1', '1.0'],
['XSAPATCH-2', '2.0'],
])
```
`findTableRows` reads cells rather than each row's whole text for the same reason the helpers above split label from value: `row.text()` concatenates the columns, so a patch named `XSAPATCH-1` at version `1.0` and one named `XSAPATCH-11` at version `.0` read identically.
### Never spell out a translation
A rendered label or a translated value is wording the component does not own, so [asserting behaviour](./assert-behaviour.md) means naming the **key**, not the English string. `src/test/i18n.ts` exposes the `t` of the very instance `createGlobalTestConfig()` installs:
@@ -159,25 +175,27 @@ Keep it to one representative value: how the formatter handles the whole range,
## Legend cards
A donut card and a progress-bar card both render their values through `UiLegend`, which keeps the label and the value addressable:
A donut card and a progress-bar card both render their values through `UiLegend`, which keeps the label and the value addressable. `src/test/find-rendered-values.ts` reads them, so a card test does not re-roll the query:
| Helper | Reads |
| ----------------------------- | ------------------------------------------------------ |
| `findLegends` | every `UiLegend`, as ordered `[label, value]` pairs |
| `findProgressBarGroupLegends` | the same, grouped per `VtsProgressBarGroup` |
| `findCardNumbers` | every `UiCardNumbers` a card lays out beside its chart |
```typescript
function findLegends(wrapper: VueWrapper) {
return wrapper
.findAll('.ui-legend')
.map(legend => [legend.get('.label').text(), legend.get('.value-and-unit').text()])
}
expect(findLegends(wrapper)).toEqual([
[t('vm:status:running', 2), '2'],
[t('vm:status:halted', 2), '1'],
])
```
Pairs rather than a record: a card may legend the same label twice, and the order it lists them in is part of what a user reads.
Three things shape those assertions:
- `VtsProgressBarGroup` sorts **descending by default**, so the expected order is the busiest first, not the order the payload listed.
- A card holding several legend groups — `PoolDashboardStatus` shows one for its hosts and one for its VMs — is queried per group (`wrapper.findAll('.vts-donut-chart-with-legend')`, then the legends inside each). A flat `findAll('.ui-legend')` collapses the groups into one list, and an assertion on it no longer says which group a value landed in.
- A card holding several legend groups — `PoolDashboardStatus` shows one for its hosts and one for its VMs — is queried per group: `findProgressBarGroupLegends` does that for progress-bar groups, while a donut card goes through `wrapper.findAll('.vts-donut-chart-with-legend')` and the legends inside each. A flat `findLegends` collapses the groups into one list, and an assertion on it no longer says which group a value landed in.
- `VtsStateHero` renders its own wording ahead of the slot: an `all-done` hero reads `'All good!Patches up to date'`. Assert the part the component owns with `toContain`.
## Routing

View File

@@ -7,7 +7,7 @@ import type {
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-labelled-values.ts'
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import UiLink from '@core/components/ui/link/UiLink.vue'

View File

@@ -4,7 +4,7 @@ import type { FrontXoPool, useXoPoolCollection } from '@/modules/pool/remote-res
import type { FrontXoVm, useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import { createPool } from '@/test/create-pool.ts'
import { createVm } from '@/test/create-vm.ts'
import { findCardLabelledValues } from '@/test/find-labelled-values.ts'
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import VtsTag from '@core/components/tag/VtsTag.vue'

View File

@@ -9,7 +9,7 @@ import type {
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-labelled-values.ts'
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'

View File

@@ -3,7 +3,7 @@ import type { FrontXoSchedule } from '@/modules/schedule/remote-resources/use-xo
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-labelled-values.ts'
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import VtsDivider from '@core/components/divider/VtsDivider.vue'

View File

@@ -2,7 +2,7 @@ import BackupJobSettingsCard from '@/modules/backup-job/components/list/panel/ca
import type { FrontAnyXoBackupJob } from '@/modules/backup-job/remote-resources/use-xo-backup-job-collection.ts'
import type { FrontXoProxy, useXoProxyCollection } from '@/modules/proxy/remote-resources/use-xo-proxy-collection.ts'
import { createMetadataBackupJob, createMirrorBackupJob, createVmBackupJob } from '@/test/create-backup-job.ts'
import { findCardLabelledValues } from '@/test/find-labelled-values.ts'
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'

View File

@@ -12,7 +12,7 @@ import type { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
import { createBackupLog, createBackupLogSrTask, createBackupLogVmTask } from '@/test/create-backup-log.ts'
import { createSr } from '@/test/create-sr.ts'
import { createVm } from '@/test/create-vm.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-labelled-values.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import UiLink from '@core/components/ui/link/UiLink.vue'

View File

@@ -6,7 +6,7 @@ import {
createBackupLogTransferTask,
createBackupLogVmTask,
} from '@/test/create-backup-log.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-labelled-values.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { formatSizeRaw } from '@core/utils/size.util.ts'

View File

@@ -1,6 +1,6 @@
import BackupRepositoryDetailAzureAzurite from '@/modules/backup-repository/components/detail/BackupRepositoryDetailAzureAzurite.vue'
import { MASKED_SECRET } from '@/modules/backup-repository/utils/xo-backup-repository.util.ts'
import { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -2,7 +2,7 @@ import BackupRepositoryDetailGeneralInfo from '@/modules/backup-repository/compo
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 { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import type { XoProxy } from '@vates/types'

View File

@@ -1,5 +1,5 @@
import BackupRepositoryDetailLocal from '@/modules/backup-repository/components/detail/BackupRepositoryDetailLocal.vue'
import { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -1,5 +1,5 @@
import BackupRepositoryDetailNfs from '@/modules/backup-repository/components/detail/BackupRepositoryDetailNfs.vue'
import { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -1,6 +1,6 @@
import BackupRepositoryDetailS3 from '@/modules/backup-repository/components/detail/BackupRepositoryDetailS3.vue'
import { MASKED_SECRET } from '@/modules/backup-repository/utils/xo-backup-repository.util.ts'
import { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -1,6 +1,6 @@
import BackupRepositoryDetailSmb from '@/modules/backup-repository/components/detail/BackupRepositoryDetailSmb.vue'
import { MASKED_SECRET } from '@/modules/backup-repository/utils/xo-backup-repository.util.ts'
import { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -2,7 +2,7 @@ import BackupRepositoryDetailSpeed from '@/modules/backup-repository/components/
import type { useXoBackupRepositoryBenchmarkJob } from '@/modules/backup-repository/jobs/xo-backup-repository-benchmark.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 { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'

View File

@@ -6,7 +6,7 @@ import {
} from '@/modules/backup-repository/utils/xo-backup-repository-details-payload.util.ts'
import { MASKED_SECRET } from '@/modules/backup-repository/utils/xo-backup-repository.util.ts'
import type { FrontXoProxy, useXoProxyCollection } from '@/modules/proxy/remote-resources/use-xo-proxy-collection.ts'
import { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mountComposable } from '@/test/mount-composable.ts'

View File

@@ -1,6 +1,6 @@
import BackupRepositoryAzureAzuriteCard from '@/modules/backup-repository/components/list/panel/cards/BackupRepositoryAzureAzuriteCard.vue'
import { MASKED_SECRET } from '@/modules/backup-repository/utils/xo-backup-repository.util.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-labelled-values.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -2,7 +2,7 @@ import BackupRepositoryInfosCard from '@/modules/backup-repository/components/li
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 { findCardCopiedValues, findCardLabelledValues } from '@/test/find-labelled-values.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import UiLink from '@core/components/ui/link/UiLink.vue'

View File

@@ -1,5 +1,5 @@
import BackupRepositoryLocalCard from '@/modules/backup-repository/components/list/panel/cards/BackupRepositoryLocalCard.vue'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-labelled-values.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -1,5 +1,5 @@
import BackupRepositoryNfsCard from '@/modules/backup-repository/components/list/panel/cards/BackupRepositoryNfsCard.vue'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-labelled-values.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -1,6 +1,6 @@
import BackupRepositoryS3Card from '@/modules/backup-repository/components/list/panel/cards/BackupRepositoryS3Card.vue'
import { MASKED_SECRET } from '@/modules/backup-repository/utils/xo-backup-repository.util.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-labelled-values.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -1,6 +1,6 @@
import BackupRepositorySmbCard from '@/modules/backup-repository/components/list/panel/cards/BackupRepositorySmbCard.vue'
import { MASKED_SECRET } from '@/modules/backup-repository/utils/xo-backup-repository.util.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-labelled-values.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -3,7 +3,7 @@ import type { useXoBackupRepositoryBenchmarkJob } from '@/modules/backup-reposit
import type { FrontXoBackupRepository } from '@/modules/backup-repository/remote-resources/use-xo-backup-repository-collection.ts'
import { createBrBenchmark } from '@/test/create-br-benchmark.ts'
import { createBr } from '@/test/create-br.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-labelled-values.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'

View File

@@ -3,7 +3,7 @@ import type { FrontXoHost, useXoHostCollection } from '@/modules/host/remote-res
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 { findCardNumbers } from '@/test/find-labelled-values.ts'
import { findCardNumbers, findLegends } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { VM_POWER_STATE } from '@vates/types'
@@ -137,7 +137,7 @@ it('labels the progress bar with the share of the cores in use', () => {
const wrapper = mountProvisioning()
expect(wrapper.get('.ui-legend').text()).toBe(`${t('vcpus')}75%`)
expect(findLegends(wrapper)).toEqual([[t('vcpus'), '75%']])
})
it('overprovisions past the cores of the host', () => {

View File

@@ -5,6 +5,7 @@ import type {
useXoHostMissingPatchesCollection,
} from '@/modules/host/remote-resources/use-xo-host-missing-patches-collection.ts'
import { createHost } from '@/test/create-host.ts'
import { findTableRows } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'
@@ -40,10 +41,6 @@ function mountPatches() {
})
}
function findPatchRows(wrapper: ReturnType<typeof mountPatches>) {
return wrapper.findAll('tbody tr').map(row => row.text())
}
it('renders the card title', () => {
const wrapper = mountPatches()
@@ -75,14 +72,17 @@ it('lists one row per missing patch', () => {
const wrapper = mountPatches()
expect(findPatchRows(wrapper)).toEqual(['XSAPATCH-11.0', 'XSAPATCH-22.0'])
expect(findTableRows(wrapper)).toEqual([
['XSAPATCH-1', '1.0'],
['XSAPATCH-2', '2.0'],
])
})
it('reports the host as up to date when it misses no patch', () => {
const wrapper = mountPatches()
expect(wrapper.get('.vts-state-hero').text()).toContain(t('patches-up-to-date'))
expect(findPatchRows(wrapper)).toEqual([])
expect(findTableRows(wrapper)).toEqual([])
})
it('reports the host as up to date while its patches are still loading', () => {

View File

@@ -2,8 +2,7 @@ import HostDashboardQuickInfo from '@/modules/host/components/dashboard/HostDash
import type { FrontXoHost, useXoHostCollection } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import type { useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import { createHost } from '@/test/create-host.ts'
import { findCopiedValues, findLabelledValues } from '@/test/find-labelled-values.ts'
import { findTags } from '@/test/find-tags.ts'
import { findCopiedValues, findLabelledValues, findTags } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { HOST_POWER_STATE } from '@vates/types'

View File

@@ -1,7 +1,7 @@
import HostDashboardRamProvisioning from '@/modules/host/components/dashboard/HostDashboardRamProvisioning.vue'
import type { FrontXoHost, useXoHostCollection } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { createHost } from '@/test/create-host.ts'
import { findCardNumbers } from '@/test/find-labelled-values.ts'
import { findCardNumbers, findLegends } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'
@@ -60,5 +60,5 @@ it('labels the progress bar with the name of the host', () => {
createHost({ name_label: 'Primary Host', memory: { size: 4294967296, usage: 1073741824 } })
)
expect(wrapper.get('.ui-legend').text()).toContain('Primary Host')
expect(findLegends(wrapper).map(([label]) => label)).toEqual(['Primary Host'])
})

View File

@@ -3,7 +3,7 @@ import type { FrontXoHost, useXoHostCollection } from '@/modules/host/remote-res
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 { findLegends } from '@/test/find-labelled-values.ts'
import { findCardNumbers, findLegends } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { VM_POWER_STATE } from '@vates/types'
@@ -102,7 +102,7 @@ it('totals the VMs of the host', () => {
const wrapper = mountVmsStatus()
expect(wrapper.get('.ui-card-numbers').text()).toBe(`${t('total')}2`)
expect(findCardNumbers(wrapper)).toEqual([[t('total'), '2']])
})
it('keeps every power state in the legend for a host without VM', () => {
@@ -121,7 +121,7 @@ it('leaves out the VMs running on another host', () => {
const wrapper = mountVmsStatus()
expect(wrapper.get('.ui-card-numbers').text()).toBe(`${t('total')}0`)
expect(findCardNumbers(wrapper)).toEqual([[t('total'), '0']])
})
it('follows the VMs of the host as the collection changes', async () => {
@@ -140,5 +140,5 @@ it('follows the VMs of the host as the collection changes', async () => {
[t('vm:status:suspended', 2), '0'],
[t('vm:status:halted', 2), '1'],
])
expect(wrapper.get('.ui-card-numbers').text()).toBe(`${t('total')}2`)
expect(findCardNumbers(wrapper)).toEqual([[t('total'), '2']])
})

View File

@@ -1,7 +1,7 @@
import HostHardwareSpecificationsCard from '@/modules/host/components/list/panel/card/HostHardwareSpecificationsCard.vue'
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { createHost } from '@/test/create-host.ts'
import { findCardLabelledValues } from '@/test/find-labelled-values.ts'
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'

View File

@@ -2,7 +2,7 @@ import HostSystemGeneralInformation from '@/modules/host/components/system/HostS
import type { FrontXoHost, useXoHostCollection } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import type { useXoPoolCollection } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
import { createHost } from '@/test/create-host.ts'
import { findCopiedValues } from '@/test/find-labelled-values.ts'
import { findCopiedValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -1,6 +1,6 @@
import HostSystemNetworking from '@/modules/host/components/system/HostSystemNetworking.vue'
import { createHost } from '@/test/create-host.ts'
import { findCopiedValues } from '@/test/find-labelled-values.ts'
import { findCopiedValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -1,8 +1,7 @@
import PluginGeneralInfo from '@/modules/plugin/components/detail/PluginGeneralInfo.vue'
import type { FrontXoPlugin } from '@/modules/plugin/remote-resources/use-xo-plugin-collection.ts'
import { createPlugin } from '@/test/create-plugin.ts'
import { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findTags } from '@/test/find-tags.ts'
import { findLabelledValues, findTags } from '@/test/find-rendered-values.ts'
import { findTitles } from '@/test/find-titles.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'

View File

@@ -2,8 +2,7 @@ import JsonSchemaValue from '@/modules/plugin/components/json-schema-form/JsonSc
import type { useXoObjectOptions } from '@/modules/plugin/composables/use-xo-object-options.composable.ts'
import type { JsonSchema } from '@/modules/plugin/utils/json-schema.util.ts'
import { createTestRouter } from '@/test/create-test-router.ts'
import { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findTags } from '@/test/find-tags.ts'
import { findLabelledValues, findTags } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import VtsStatus from '@core/components/status/VtsStatus.vue'

View File

@@ -1,7 +1,7 @@
import PluginInfosCard from '@/modules/plugin/components/list/panel/cards/PluginInfosCard.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 { findCardCopiedValues, findCardLabelledValues } from '@/test/find-labelled-values.ts'
import { findCardCopiedValues, findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -1,6 +1,6 @@
import PoolDashboardCpuProvisioning from '@/modules/pool/components/dashboard/PoolDashboardCpuProvisioning.vue'
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import { findCardNumbers } from '@/test/find-labelled-values.ts'
import { findCardNumbers, findLegends } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'
@@ -61,7 +61,7 @@ it('fills the progress bar with the share of the CPUs assigned', () => {
it('labels the progress bar with the share of the CPUs assigned', () => {
const wrapper = mountProvisioning()
expect(wrapper.get('.ui-legend').text()).toBe(`${t('vcpus')}75%`)
expect(findLegends(wrapper)).toEqual([[t('vcpus'), '75%']])
})
it('reports no CPU for a pool provisioning nothing', () => {

View File

@@ -1,20 +1,29 @@
import HostsCpuUsage from '@/modules/pool/components/dashboard/cpu-usage/HostsCpuUsage.vue'
import VmsCpuUsage from '@/modules/pool/components/dashboard/cpu-usage/VmsCpuUsage.vue'
import PoolDashboardCpuUsage from '@/modules/pool/components/dashboard/PoolDashboardCpuUsage.vue'
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import { findLegends } from '@/test/find-labelled-values.ts'
import { createPoolDashboardTopFiveUsage } from '@/test/create-pool-dashboard.ts'
import { findProgressBarGroupLegends } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'
type TopFiveUsage = NonNullable<NonNullable<XoPoolDashboard['hosts']>['topFiveUsage']>
type HostCpuUsage = NonNullable<NonNullable<XoPoolDashboard['hosts']>['topFiveUsage']>['cpu'][number]
type VmCpuUsage = NonNullable<NonNullable<XoPoolDashboard['vms']>['topFiveUsage']>['cpu'][number]
function createHostCpuUsage(overrides: Partial<HostCpuUsage> = {}): HostCpuUsage {
return { id: 'host-1' as HostCpuUsage['id'], name_label: 'Host 1', percent: 30, ...overrides }
}
function createVmCpuUsage(overrides: Partial<VmCpuUsage> = {}): VmCpuUsage {
return { id: 'vm-1' as VmCpuUsage['id'], name_label: 'VM 1', percent: 70, ...overrides }
}
function withTopFiveCpu(hosts: HostCpuUsage[], vms: VmCpuUsage[]): XoPoolDashboard {
return createPoolDashboardTopFiveUsage({ cpu: hosts }, { cpu: vms })
}
function createPoolDashboard(overrides: Partial<XoPoolDashboard> = {}): XoPoolDashboard {
return {
hosts: { topFiveUsage: { cpu: [], ram: [] } as TopFiveUsage },
vms: { topFiveUsage: { cpu: [], ram: [] } as NonNullable<NonNullable<XoPoolDashboard['vms']>['topFiveUsage']> },
...overrides,
}
return { ...withTopFiveCpu([createHostCpuUsage()], [createVmCpuUsage()]), ...overrides }
}
function mountCpuUsage(props: { poolDashboard?: XoPoolDashboard; hasError?: boolean } = {}) {
@@ -39,47 +48,46 @@ it('splits the card between the hosts and the VMs, each capped to the top five',
])
})
it('plots the usage of the hosts and of the VMs in their own section', () => {
const wrapper = mountCpuUsage()
expect(findProgressBarGroupLegends(wrapper)).toEqual([[['Host 1', '30%']], [['VM 1', '70%']]])
})
it('shows a loader in place of the hosts while their usage has not arrived yet', () => {
const wrapper = mountCpuUsage({ poolDashboard: createPoolDashboard({ hosts: {} }) })
expect(wrapper.find('.ui-loader').exists()).toBe(true)
expect(wrapper.findComponent(HostsCpuUsage).exists()).toBe(false)
expect(wrapper.findComponent(VmsCpuUsage).exists()).toBe(true)
expect(wrapper.findAll('.ui-loader')).toHaveLength(1)
expect(findProgressBarGroupLegends(wrapper)).toEqual([[['VM 1', '70%']]])
})
it('shows a loader in place of the VMs while their usage has not arrived yet', () => {
const wrapper = mountCpuUsage({ poolDashboard: createPoolDashboard({ vms: {} }) })
expect(wrapper.find('.ui-loader').exists()).toBe(true)
expect(wrapper.findComponent(HostsCpuUsage).exists()).toBe(true)
expect(wrapper.findComponent(VmsCpuUsage).exists()).toBe(false)
expect(wrapper.findAll('.ui-loader')).toHaveLength(1)
expect(findProgressBarGroupLegends(wrapper)).toEqual([[['Host 1', '30%']]])
})
it('plots the usage of the hosts and of the VMs in their own section', () => {
it('measures both sections against the CPU thresholds, which a full CPU has yet to reach', () => {
const wrapper = mountCpuUsage({
poolDashboard: {
hosts: {
topFiveUsage: {
cpu: [{ id: 'host-1' as never, name_label: 'Host 1', percent: 30 }],
ram: [],
} as unknown as TopFiveUsage,
},
vms: {
topFiveUsage: {
cpu: [{ id: 'vm-1' as never, name_label: 'VM 1', percent: 70 }],
ram: [],
} as unknown as NonNullable<NonNullable<XoPoolDashboard['vms']>['topFiveUsage']>,
},
},
poolDashboard: withTopFiveCpu([createHostCpuUsage({ percent: 85 })], [createVmCpuUsage({ percent: 85 })]),
})
expect(findLegends(wrapper.findComponent(HostsCpuUsage))).toEqual([['Host 1', '30%']])
expect(findLegends(wrapper.findComponent(VmsCpuUsage))).toEqual([['VM 1', '70%']])
expect(wrapper.findAll('.ui-progress-bar').map(bar => bar.classes('accent--info'))).toEqual([true, true])
})
it('reports that there is nothing to show in a section without host or VM', () => {
const wrapper = mountCpuUsage({ poolDashboard: withTopFiveCpu([], []) })
expect(wrapper.findAll('.vts-state-hero').map(hero => hero.text())).toEqual([
t('no-data-to-calculate'),
t('no-data-to-calculate'),
])
expect(wrapper.findAll('.ui-progress-bar')).toEqual([])
})
it('passes the fetch error down to both sections', () => {
const wrapper = mountCpuUsage({ hasError: true })
expect(wrapper.findComponent(HostsCpuUsage).get('.vts-state-hero').text()).toBe(t('error-no-data'))
expect(wrapper.findComponent(VmsCpuUsage).get('.vts-state-hero').text()).toBe(t('error-no-data'))
expect(wrapper.findAll('.vts-state-hero').map(hero => hero.text())).toEqual([t('error-no-data'), t('error-no-data')])
})

View File

@@ -9,31 +9,37 @@
{{ t('top-#', 5) }}
</template>
</UiCardSubtitle>
<VtsStateHero v-if="!areHostsCpuUsageReady" format="card" type="busy" size="medium" />
<template v-else>
<HostsCpuUsage :top-five-cpu="poolDashboard?.hosts?.topFiveUsage?.cpu" :has-error />
</template>
<VtsProgressBarGroupWithStates
:items="hostsCpuItems ?? []"
:thresholds="cpuThresholds"
:has-error
:busy="hostsCpuItems === undefined"
legend-type="percent"
/>
<UiCardSubtitle>
{{ t('vms') }}
<template #info>
{{ t('top-#', 5) }}
</template>
</UiCardSubtitle>
<VtsStateHero v-if="!areVmsCpuUsageReady" format="card" type="busy" size="medium" />
<template v-else>
<VmsCpuUsage :top-five-cpu="poolDashboard?.vms?.topFiveUsage?.cpu" :has-error />
</template>
<VtsProgressBarGroupWithStates
:items="vmsCpuItems ?? []"
:thresholds="cpuThresholds"
:has-error
:busy="vmsCpuItems === undefined"
legend-type="percent"
/>
</UiCard>
</template>
<script lang="ts" setup>
import HostsCpuUsage from '@/modules/pool/components/dashboard/cpu-usage/HostsCpuUsage.vue'
import VmsCpuUsage from '@/modules/pool/components/dashboard/cpu-usage/VmsCpuUsage.vue'
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import { toPercentProgressItem } from '@/modules/pool/utils/xo-pool-dashboard.util.ts'
import VtsProgressBarGroupWithStates from '@core/components/progress-bar-group-with-states/VtsProgressBarGroupWithStates.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardSubtitle from '@core/components/ui/card-subtitle/UiCardSubtitle.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import { cpuProgressThresholds } from '@core/utils/progress.util.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -42,9 +48,11 @@ const { poolDashboard } = defineProps<{
hasError?: boolean
}>()
const areHostsCpuUsageReady = computed(() => poolDashboard?.hosts?.topFiveUsage?.cpu !== undefined)
const cpuThresholds = cpuProgressThresholds()
const areVmsCpuUsageReady = computed(() => poolDashboard?.vms?.topFiveUsage?.cpu !== undefined)
const hostsCpuItems = computed(() => poolDashboard?.hosts?.topFiveUsage?.cpu?.map(toPercentProgressItem))
const vmsCpuItems = computed(() => poolDashboard?.vms?.topFiveUsage?.cpu?.map(toPercentProgressItem))
const { t } = useI18n()
</script>

View File

@@ -1,5 +1,6 @@
import PoolDashboardHostsPatches from '@/modules/pool/components/dashboard/PoolDashboardHostsPatches.vue'
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import { findTableRows } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'
@@ -24,10 +25,6 @@ function mountPatches(props: { poolDashboard?: XoPoolDashboard; hasError?: boole
})
}
function findPatchRows(wrapper: ReturnType<typeof mountPatches>) {
return wrapper.findAll('tbody tr').map(row => row.text())
}
it('renders the card title', () => {
const wrapper = mountPatches()
@@ -38,7 +35,7 @@ it('shows a loader while the dashboard has not arrived yet', () => {
const wrapper = mountPatches({ poolDashboard: undefined })
expect(wrapper.find('.ui-loader').exists()).toBe(true)
expect(findPatchRows(wrapper)).toEqual([])
expect(findTableRows(wrapper)).toEqual([])
})
it('shows a loader while the missing patches are missing from the dashboard', () => {
@@ -66,7 +63,10 @@ it('lists one row per missing patch', () => {
]),
})
expect(findPatchRows(wrapper)).toEqual(['XSAPATCH-11.0', 'XSAPATCH-22.0'])
expect(findTableRows(wrapper)).toEqual([
['XSAPATCH-1', '1.0'],
['XSAPATCH-2', '2.0'],
])
})
it('reports the pool as up to date when its hosts miss no patch', () => {
@@ -80,5 +80,5 @@ it('reports the pool as up to date when the patches cannot be listed without a l
const wrapper = mountPatches({ poolDashboard: withMissingPatches({ hasAuthorization: false }) })
expect(wrapper.get('.vts-state-hero').text()).toContain(t('patches-up-to-date'))
expect(findPatchRows(wrapper)).toEqual([])
expect(findTableRows(wrapper)).toEqual([])
})

View File

@@ -1,25 +1,43 @@
import PoolDashboardRamUsage from '@/modules/pool/components/dashboard/PoolDashboardRamUsage.vue'
import HostsRamUsage from '@/modules/pool/components/dashboard/ram-usage/HostsRamUsage.vue'
import VmsRamUsage from '@/modules/pool/components/dashboard/ram-usage/VmsRamUsage.vue'
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import { findLegends } from '@/test/find-labelled-values.ts'
import { createPoolDashboardTopFiveUsage } from '@/test/create-pool-dashboard.ts'
import { findProgressBarGroupLegends } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'
type HostsTopFiveUsage = NonNullable<NonNullable<XoPoolDashboard['hosts']>['topFiveUsage']>
type HostRamUsage = NonNullable<NonNullable<XoPoolDashboard['hosts']>['topFiveUsage']>['ram'][number]
type VmsTopFiveUsage = NonNullable<NonNullable<XoPoolDashboard['vms']>['topFiveUsage']>
type VmRamUsage = NonNullable<NonNullable<XoPoolDashboard['vms']>['topFiveUsage']>['ram'][number]
function withTopFiveUsage(hosts: HostsTopFiveUsage['ram'], vms: VmsTopFiveUsage['ram']): XoPoolDashboard {
function createHostRamUsage(overrides: Partial<HostRamUsage> = {}): HostRamUsage {
return {
hosts: { topFiveUsage: { cpu: [], ram: hosts } },
vms: { topFiveUsage: { cpu: [], ram: vms } },
id: 'host-1' as HostRamUsage['id'],
name_label: 'Host 1',
percent: 25,
size: 4294967296,
usage: 1073741824,
...overrides,
}
}
function createVmRamUsage(overrides: Partial<VmRamUsage> = {}): VmRamUsage {
return {
id: 'vm-1' as VmRamUsage['id'],
name_label: 'VM 1',
percent: 75,
memory: 4294967296,
memoryFree: 1073741824,
...overrides,
}
}
function withTopFiveRam(hosts: HostRamUsage[], vms: VmRamUsage[]): XoPoolDashboard {
return createPoolDashboardTopFiveUsage({ ram: hosts }, { ram: vms })
}
function createPoolDashboard(overrides: Partial<XoPoolDashboard> = {}): XoPoolDashboard {
return { ...withTopFiveUsage([], []), ...overrides }
return { ...withTopFiveRam([createHostRamUsage()], [createVmRamUsage()]), ...overrides }
}
function mountRamUsage(props: { poolDashboard?: XoPoolDashboard; hasError?: boolean } = {}) {
@@ -44,53 +62,38 @@ it('splits the card between the hosts and the VMs, each capped to the top five',
])
})
it('shows the memory of the hosts and of the VMs in their own section', () => {
const wrapper = mountRamUsage()
expect(findProgressBarGroupLegends(wrapper)).toEqual([[['Host 1', '1 GiB / 4 GiB']], [['VM 1', '3 GiB / 4 GiB']]])
})
it('shows a loader in place of the hosts while their usage has not arrived yet', () => {
const wrapper = mountRamUsage({ poolDashboard: createPoolDashboard({ hosts: {} }) })
expect(wrapper.find('.ui-loader').exists()).toBe(true)
expect(wrapper.findComponent(HostsRamUsage).exists()).toBe(false)
expect(wrapper.findComponent(VmsRamUsage).exists()).toBe(true)
expect(wrapper.findAll('.ui-loader')).toHaveLength(1)
expect(findProgressBarGroupLegends(wrapper)).toEqual([[['VM 1', '3 GiB / 4 GiB']]])
})
it('shows a loader in place of the VMs while their usage has not arrived yet', () => {
const wrapper = mountRamUsage({ poolDashboard: createPoolDashboard({ vms: {} }) })
expect(wrapper.find('.ui-loader').exists()).toBe(true)
expect(wrapper.findComponent(HostsRamUsage).exists()).toBe(true)
expect(wrapper.findComponent(VmsRamUsage).exists()).toBe(false)
expect(wrapper.findAll('.ui-loader')).toHaveLength(1)
expect(findProgressBarGroupLegends(wrapper)).toEqual([[['Host 1', '1 GiB / 4 GiB']]])
})
it('shows the memory of the hosts and of the VMs in their own section', () => {
const wrapper = mountRamUsage({
poolDashboard: withTopFiveUsage(
[
{
id: 'host-1' as HostsTopFiveUsage['ram'][number]['id'],
name_label: 'Host 1',
percent: 25,
size: 4294967296,
usage: 1073741824,
},
],
[
{
id: 'vm-1' as VmsTopFiveUsage['ram'][number]['id'],
name_label: 'VM 1',
percent: 75,
memory: 4294967296,
memoryFree: 1073741824,
},
]
),
})
it('reports that there is nothing to show in a section without host or VM', () => {
const wrapper = mountRamUsage({ poolDashboard: withTopFiveRam([], []) })
expect(findLegends(wrapper.findComponent(HostsRamUsage))).toEqual([['Host 1', '1 GiB / 4 GiB']])
expect(findLegends(wrapper.findComponent(VmsRamUsage))).toEqual([['VM 1', '3 GiB / 4 GiB']])
expect(wrapper.findAll('.vts-state-hero').map(hero => hero.text())).toEqual([
t('no-data-to-calculate'),
t('no-data-to-calculate'),
])
expect(wrapper.findAll('.ui-progress-bar')).toEqual([])
})
it('passes the fetch error down to both sections', () => {
const wrapper = mountRamUsage({ hasError: true })
expect(wrapper.findComponent(HostsRamUsage).get('.vts-state-hero').text()).toBe(t('error-no-data'))
expect(wrapper.findComponent(VmsRamUsage).get('.vts-state-hero').text()).toBe(t('error-no-data'))
expect(wrapper.findAll('.vts-state-hero').map(hero => hero.text())).toEqual([t('error-no-data'), t('error-no-data')])
})

View File

@@ -9,28 +9,31 @@
{{ t('top-#', 5) }}
</template>
</UiCardSubtitle>
<VtsStateHero v-if="!areHostsRamUsageReady" format="card" type="busy" size="medium" />
<template v-else>
<HostsRamUsage :top-five-ram="poolDashboard?.hosts?.topFiveUsage?.ram" :has-error />
</template>
<VtsProgressBarGroupWithStates
:items="hostsRamItems ?? []"
:has-error
:busy="hostsRamItems === undefined"
legend-type="bytes-with-total"
/>
<UiCardSubtitle>
{{ t('vms') }}
<template #info>
{{ t('top-#', 5) }}
</template>
</UiCardSubtitle>
<VtsStateHero v-if="!areVmsRamUsageReady" format="card" type="busy" size="medium" />
<template v-else>
<VmsRamUsage :top-five-ram="poolDashboard?.vms?.topFiveUsage?.ram" :has-error />
</template>
<VtsProgressBarGroupWithStates
:items="vmsRamItems ?? []"
:has-error
:busy="vmsRamItems === undefined"
legend-type="bytes-with-total"
/>
</UiCard>
</template>
<script lang="ts" setup>
import HostsRamUsage from '@/modules/pool/components/dashboard/ram-usage/HostsRamUsage.vue'
import VmsRamUsage from '@/modules/pool/components/dashboard/ram-usage/VmsRamUsage.vue'
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import { toHostRamProgressItem, toVmRamProgressItem } from '@/modules/pool/utils/xo-pool-dashboard.util.ts'
import VtsProgressBarGroupWithStates from '@core/components/progress-bar-group-with-states/VtsProgressBarGroupWithStates.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardSubtitle from '@core/components/ui/card-subtitle/UiCardSubtitle.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
@@ -42,8 +45,9 @@ const { poolDashboard } = defineProps<{
hasError?: boolean
}>()
const areHostsRamUsageReady = computed(() => poolDashboard?.hosts?.topFiveUsage?.ram !== undefined)
const areVmsRamUsageReady = computed(() => poolDashboard?.vms?.topFiveUsage?.ram !== undefined)
const hostsRamItems = computed(() => poolDashboard?.hosts?.topFiveUsage?.ram?.map(toHostRamProgressItem))
const vmsRamItems = computed(() => poolDashboard?.vms?.topFiveUsage?.ram?.map(toVmRamProgressItem))
const { t } = useI18n()
</script>

View File

@@ -1,6 +1,6 @@
import PoolDashboardStatus from '@/modules/pool/components/dashboard/PoolDashboardStatus.vue'
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import { findCardNumbers } from '@/test/find-labelled-values.ts'
import { findCardNumbers, findLegends } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'
@@ -17,7 +17,7 @@ function createVmsStatus(overrides: Partial<VmsStatus> = {}): VmsStatus {
return { running: 5, paused: 2, suspended: 1, halted: 2, total: 10, ...overrides }
}
function createPoolDashboard(overrides: Partial<XoPoolDashboard> = {}): XoPoolDashboard {
function createPoolDashboardStatus(overrides: Partial<XoPoolDashboard> = {}): XoPoolDashboard {
return {
hosts: { status: createHostsStatus() },
vms: { status: createVmsStatus() },
@@ -27,7 +27,7 @@ function createPoolDashboard(overrides: Partial<XoPoolDashboard> = {}): XoPoolDa
function mountStatus(props: { poolDashboard?: XoPoolDashboard; hasError?: boolean } = {}) {
return mount(PoolDashboardStatus, {
props: { poolDashboard: createPoolDashboard(), ...props },
props: { poolDashboard: createPoolDashboardStatus(), ...props },
global: createGlobalTestConfig(),
})
}
@@ -35,10 +35,7 @@ function mountStatus(props: { poolDashboard?: XoPoolDashboard; hasError?: boolea
function findLegendSections(wrapper: ReturnType<typeof mountStatus>) {
return wrapper
.findAll('.vts-donut-chart-with-legend')
.map(section => [
section.get('.ui-legend-title').text(),
section.findAll('.ui-legend').map(legend => [legend.get('.label').text(), legend.get('.value-and-unit').text()]),
])
.map(section => [section.get('.ui-legend-title').text(), findLegends(section)])
}
it('renders the card title', () => {
@@ -107,7 +104,7 @@ it('totals the hosts and the VMs of the pool', () => {
it('reports that no VM was detected for a pool without VM, and still breaks down the hosts', () => {
const wrapper = mountStatus({
poolDashboard: createPoolDashboard({
poolDashboard: createPoolDashboardStatus({
vms: { status: createVmsStatus({ running: 0, paused: 0, suspended: 0, halted: 0, total: 0 }) },
}),
})

View File

@@ -1,6 +1,6 @@
import PoolDashboardStoragesUsage from '@/modules/pool/components/dashboard/PoolDashboardStoragesUsage.vue'
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import { findCardNumbers } from '@/test/find-labelled-values.ts'
import { findCardNumbers, findLegends } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'
@@ -68,7 +68,10 @@ it('shows one progress bar per storage, the fullest first', () => {
]),
})
expect(wrapper.findAll('.ui-legend').map(legend => legend.text())).toEqual(['Shared storage70%', 'Local storage30%'])
expect(findLegends(wrapper)).toEqual([
['Shared storage', '70%'],
['Local storage', '30%'],
])
})
it('sums the usage and the free space across the storages', () => {

View File

@@ -6,15 +6,12 @@
{{ t('top-#', 5) }}
</template>
</UiCardTitle>
<VtsStateHero v-if="!areStoragesUsageReady" format="card" type="busy" size="medium" />
<VtsStateHero v-else-if="hasError" format="card" type="error" size="medium">
{{ t('error-no-data') }}
</VtsStateHero>
<VtsStateHero v-else-if="topFiveUsage.length === 0" format="card" type="no-data" size="medium">
{{ t('no-data-to-calculate') }}
</VtsStateHero>
<template v-else>
<VtsProgressBarGroup :items="progressBarItems" legend-type="percent" />
<VtsProgressBarGroupWithStates
:items="progressBarItems"
:busy="!areStoragesUsageReady"
:has-error
legend-type="percent"
>
<div class="total">
<UiCardNumbers
:label="t('total-used')"
@@ -29,15 +26,14 @@
size="medium"
/>
</div>
</template>
</VtsProgressBarGroupWithStates>
</UiCard>
</template>
<script lang="ts" setup>
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import { buildPercentProgressItems, getStoragesUsageTotals } from '@/modules/pool/utils/xo-pool-dashboard.util.ts'
import VtsProgressBarGroup from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import { getStoragesUsageTotals, toPercentProgressItem } from '@/modules/pool/utils/xo-pool-dashboard.util.ts'
import VtsProgressBarGroupWithStates from '@core/components/progress-bar-group-with-states/VtsProgressBarGroupWithStates.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardNumbers from '@core/components/ui/card-numbers/UiCardNumbers.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
@@ -61,7 +57,7 @@ const totals = computed(() => getStoragesUsageTotals(topFiveUsage.value))
const formattedTotalUsage = computed(() => formatSizeRaw(totals.value.totalUsage, 0))
const formattedTotalSizeFree = computed(() => formatSizeRaw(totals.value.totalSize - totals.value.totalUsage, 0))
const progressBarItems = computed(() => buildPercentProgressItems(topFiveUsage.value))
const progressBarItems = computed(() => topFiveUsage.value.map(toPercentProgressItem))
</script>
<style lang="postcss" scoped>

View File

@@ -1,63 +0,0 @@
import HostsCpuUsage from '@/modules/pool/components/dashboard/cpu-usage/HostsCpuUsage.vue'
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import { findLegends } from '@/test/find-labelled-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'
type HostCpuUsage = NonNullable<NonNullable<XoPoolDashboard['hosts']>['topFiveUsage']>['cpu'][number]
function createHostCpuUsage(overrides: Partial<HostCpuUsage> = {}): HostCpuUsage {
return {
id: 'host-1' as HostCpuUsage['id'],
name_label: 'Host 1',
percent: 25,
...overrides,
}
}
function mountCpuUsage(props: { topFiveCpu?: HostCpuUsage[]; hasError?: boolean } = {}) {
return mount(HostsCpuUsage, {
props: { topFiveCpu: undefined, ...props },
global: createGlobalTestConfig(),
})
}
it('reports that there is nothing to show while the usage has not arrived yet', () => {
const wrapper = mountCpuUsage()
expect(wrapper.get('.vts-state-hero').text()).toBe(t('no-data-to-calculate'))
})
it('shows an error message when the usage could not be fetched', () => {
const wrapper = mountCpuUsage({ topFiveCpu: [createHostCpuUsage()], hasError: true })
expect(wrapper.get('.vts-state-hero').text()).toBe(t('error-no-data'))
})
it('shows one progress bar per host, the busiest first', () => {
const wrapper = mountCpuUsage({
topFiveCpu: [
createHostCpuUsage({ id: 'host-1' as HostCpuUsage['id'], name_label: 'Host 1', percent: 30 }),
createHostCpuUsage({ id: 'host-2' as HostCpuUsage['id'], name_label: 'Host 2', percent: 70 }),
],
})
expect(findLegends(wrapper)).toEqual([
['Host 2', '70%'],
['Host 1', '30%'],
])
})
it('reads each share of usage out of a hundred', () => {
const wrapper = mountCpuUsage({ topFiveCpu: [createHostCpuUsage({ percent: 12.5 })] })
expect(findLegends(wrapper)).toEqual([['Host 1', '12.5%']])
})
it('shows an empty progress bar group for a pool without host', () => {
const wrapper = mountCpuUsage({ topFiveCpu: [] })
expect(wrapper.findAll('.ui-progress-bar')).toEqual([])
expect(wrapper.find('.vts-state-hero').exists()).toBe(false)
})

View File

@@ -1,31 +0,0 @@
<template>
<VtsStateHero v-if="topFiveCpu === undefined" format="card" type="no-data" size="medium">
{{ t('no-data-to-calculate') }}
</VtsStateHero>
<VtsStateHero v-else-if="hasError" format="card" type="error" size="medium">
{{ t('error-no-data') }}
</VtsStateHero>
<template v-else>
<VtsProgressBarGroup :items="progressBarItems" :thresholds="cpuProgressThresholds()" legend-type="percent" />
</template>
</template>
<script lang="ts" setup>
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import { buildPercentProgressItems } from '@/modules/pool/utils/xo-pool-dashboard.util.ts'
import VtsProgressBarGroup from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import { cpuProgressThresholds } from '@core/utils/progress.util.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
// TODO: refactor to use the same component as VmsCpuUsage.vue
const { topFiveCpu } = defineProps<{
topFiveCpu: NonNullable<NonNullable<XoPoolDashboard['hosts']>['topFiveUsage']>['cpu'] | undefined
hasError?: boolean
}>()
const progressBarItems = computed(() => buildPercentProgressItems(topFiveCpu ?? []))
const { t } = useI18n()
</script>

View File

@@ -1,57 +0,0 @@
import VmsCpuUsage from '@/modules/pool/components/dashboard/cpu-usage/VmsCpuUsage.vue'
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import { findLegends } from '@/test/find-labelled-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'
type VmCpuUsage = NonNullable<NonNullable<XoPoolDashboard['vms']>['topFiveUsage']>['cpu'][number]
function createVmCpuUsage(overrides: Partial<VmCpuUsage> = {}): VmCpuUsage {
return {
id: 'vm-1' as VmCpuUsage['id'],
name_label: 'VM 1',
percent: 25,
...overrides,
}
}
function mountCpuUsage(props: { topFiveCpu?: VmCpuUsage[]; hasError?: boolean } = {}) {
return mount(VmsCpuUsage, {
props: { topFiveCpu: undefined, ...props },
global: createGlobalTestConfig(),
})
}
it('reports that there is nothing to show while the usage has not arrived yet', () => {
const wrapper = mountCpuUsage()
expect(wrapper.get('.vts-state-hero').text()).toBe(t('no-data-to-calculate'))
})
it('shows an error message when the usage could not be fetched', () => {
const wrapper = mountCpuUsage({ topFiveCpu: [createVmCpuUsage()], hasError: true })
expect(wrapper.get('.vts-state-hero').text()).toBe(t('error-no-data'))
})
it('shows one progress bar per VM, the busiest first', () => {
const wrapper = mountCpuUsage({
topFiveCpu: [
createVmCpuUsage({ id: 'vm-1' as VmCpuUsage['id'], name_label: 'VM 1', percent: 30 }),
createVmCpuUsage({ id: 'vm-2' as VmCpuUsage['id'], name_label: 'VM 2', percent: 70 }),
],
})
expect(findLegends(wrapper)).toEqual([
['VM 2', '70%'],
['VM 1', '30%'],
])
})
it('shows an empty progress bar group for a pool without VM', () => {
const wrapper = mountCpuUsage({ topFiveCpu: [] })
expect(wrapper.findAll('.ui-progress-bar')).toEqual([])
expect(wrapper.find('.vts-state-hero').exists()).toBe(false)
})

View File

@@ -1,31 +0,0 @@
<template>
<VtsStateHero v-if="topFiveCpu === undefined" format="card" type="no-data" size="medium">
{{ t('no-data-to-calculate') }}
</VtsStateHero>
<VtsStateHero v-else-if="hasError" format="card" type="error" size="medium">
{{ t('error-no-data') }}
</VtsStateHero>
<template v-else>
<VtsProgressBarGroup :items="progressBarItems" :thresholds="cpuProgressThresholds()" legend-type="percent" />
</template>
</template>
<script lang="ts" setup>
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import { buildPercentProgressItems } from '@/modules/pool/utils/xo-pool-dashboard.util.ts'
import VtsProgressBarGroup from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import { cpuProgressThresholds } from '@core/utils/progress.util.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
// TODO: refactor to use the same component as HostsCpuUsage.vue
const { topFiveCpu } = defineProps<{
topFiveCpu: NonNullable<NonNullable<XoPoolDashboard['vms']>['topFiveUsage']>['cpu'] | undefined
hasError?: boolean
}>()
const { t } = useI18n()
const progressBarItems = computed(() => buildPercentProgressItems(topFiveCpu ?? []))
</script>

View File

@@ -1,77 +0,0 @@
import HostsRamUsage from '@/modules/pool/components/dashboard/ram-usage/HostsRamUsage.vue'
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import { findLegends } from '@/test/find-labelled-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'
type HostRamUsage = NonNullable<NonNullable<XoPoolDashboard['hosts']>['topFiveUsage']>['ram'][number]
function createHostRamUsage(overrides: Partial<HostRamUsage> = {}): HostRamUsage {
return {
id: 'host-1' as HostRamUsage['id'],
name_label: 'Host 1',
percent: 25,
size: 4294967296,
usage: 1073741824,
...overrides,
}
}
function mountRamUsage(props: { topFiveRam?: HostRamUsage[]; hasError?: boolean } = {}) {
return mount(HostsRamUsage, {
props: { topFiveRam: undefined, ...props },
global: createGlobalTestConfig(),
})
}
it('reports that there is nothing to show while the usage has not arrived yet', () => {
const wrapper = mountRamUsage()
expect(wrapper.get('.vts-state-hero').text()).toBe(t('no-data-to-calculate'))
})
it('shows an error message when the usage could not be fetched', () => {
const wrapper = mountRamUsage({ topFiveRam: [createHostRamUsage()], hasError: true })
expect(wrapper.get('.vts-state-hero').text()).toBe(t('error-no-data'))
})
it('shows the memory each host uses out of the memory it holds, the fullest first', () => {
const wrapper = mountRamUsage({
topFiveRam: [
createHostRamUsage({
id: 'host-1' as HostRamUsage['id'],
name_label: 'Host 1',
usage: 1073741824,
size: 4294967296,
}),
createHostRamUsage({
id: 'host-2' as HostRamUsage['id'],
name_label: 'Host 2',
usage: 3221225472,
size: 4294967296,
}),
],
})
expect(findLegends(wrapper)).toEqual([
['Host 2', '3 GiB / 4 GiB'],
['Host 1', '1 GiB / 4 GiB'],
])
})
it('shows an empty progress bar group for a pool without host', () => {
const wrapper = mountRamUsage({ topFiveRam: [] })
expect(wrapper.findAll('.ui-progress-bar')).toEqual([])
expect(wrapper.find('.vts-state-hero').exists()).toBe(false)
})
it('follows the usage as new stats arrive', async () => {
const wrapper = mountRamUsage({ topFiveRam: [createHostRamUsage({ usage: 1073741824, size: 4294967296 })] })
await wrapper.setProps({ topFiveRam: [createHostRamUsage({ usage: 3221225472, size: 4294967296 })] })
expect(findLegends(wrapper)).toEqual([['Host 1', '3 GiB / 4 GiB']])
})

View File

@@ -1,30 +0,0 @@
<template>
<VtsStateHero v-if="topFiveRam === undefined" format="card" type="no-data" size="medium">
{{ t('no-data-to-calculate') }}
</VtsStateHero>
<VtsStateHero v-else-if="hasError" format="card" type="error" size="medium">
{{ t('error-no-data') }}
</VtsStateHero>
<template v-else>
<VtsProgressBarGroup :items="progressBarItems" legend-type="bytes-with-total" />
</template>
</template>
<script lang="ts" setup>
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import { buildHostsRamProgressItems } from '@/modules/pool/utils/xo-pool-dashboard.util.ts'
import VtsProgressBarGroup from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
// TODO: refactor to use the same component as VmsRamUsage.vue
const { topFiveRam } = defineProps<{
topFiveRam: NonNullable<NonNullable<XoPoolDashboard['hosts']>['topFiveUsage']>['ram'] | undefined
hasError?: boolean
}>()
const { t } = useI18n()
const progressBarItems = computed(() => buildHostsRamProgressItems(topFiveRam ?? []))
</script>

View File

@@ -1,69 +0,0 @@
import VmsRamUsage from '@/modules/pool/components/dashboard/ram-usage/VmsRamUsage.vue'
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import { findLegends } from '@/test/find-labelled-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'
type VmRamUsage = NonNullable<NonNullable<XoPoolDashboard['vms']>['topFiveUsage']>['ram'][number]
function createVmRamUsage(overrides: Partial<VmRamUsage> = {}): VmRamUsage {
return {
id: 'vm-1' as VmRamUsage['id'],
name_label: 'VM 1',
percent: 25,
memory: 4294967296,
memoryFree: 3221225472,
...overrides,
}
}
function mountRamUsage(props: { topFiveRam?: VmRamUsage[]; hasError?: boolean } = {}) {
return mount(VmsRamUsage, {
props: { topFiveRam: undefined, ...props },
global: createGlobalTestConfig(),
})
}
it('reports that there is nothing to show while the usage has not arrived yet', () => {
const wrapper = mountRamUsage()
expect(wrapper.get('.vts-state-hero').text()).toBe(t('no-data-to-calculate'))
})
it('shows an error message when the usage could not be fetched', () => {
const wrapper = mountRamUsage({ topFiveRam: [createVmRamUsage()], hasError: true })
expect(wrapper.get('.vts-state-hero').text()).toBe(t('error-no-data'))
})
it('shows the memory each VM uses out of the memory it holds, the fullest first', () => {
const wrapper = mountRamUsage({
topFiveRam: [
createVmRamUsage({
id: 'vm-1' as VmRamUsage['id'],
name_label: 'VM 1',
memory: 4294967296,
memoryFree: 3221225472,
}),
createVmRamUsage({
id: 'vm-2' as VmRamUsage['id'],
name_label: 'VM 2',
memory: 4294967296,
memoryFree: 1073741824,
}),
],
})
expect(findLegends(wrapper)).toEqual([
['VM 2', '3 GiB / 4 GiB'],
['VM 1', '1 GiB / 4 GiB'],
])
})
it('shows an empty progress bar group for a pool without VM', () => {
const wrapper = mountRamUsage({ topFiveRam: [] })
expect(wrapper.findAll('.ui-progress-bar')).toEqual([])
expect(wrapper.find('.vts-state-hero').exists()).toBe(false)
})

View File

@@ -1,30 +0,0 @@
<template>
<VtsStateHero v-if="topFiveRam === undefined" format="card" type="no-data" size="medium">
{{ t('no-data-to-calculate') }}
</VtsStateHero>
<VtsStateHero v-else-if="hasError" format="card" type="error" size="medium">
{{ t('error-no-data') }}
</VtsStateHero>
<template v-else>
<VtsProgressBarGroup :items="progressBarItems" legend-type="bytes-with-total" />
</template>
</template>
<script lang="ts" setup>
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
import { buildVmsRamProgressItems } from '@/modules/pool/utils/xo-pool-dashboard.util.ts'
import VtsProgressBarGroup from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
// TODO: refactor to use the same component as HostsRamUsage.vue
const { topFiveRam } = defineProps<{
topFiveRam: NonNullable<NonNullable<XoPoolDashboard['vms']>['topFiveUsage']>['ram'] | undefined
hasError?: boolean
}>()
const { t } = useI18n()
const progressBarItems = computed(() => buildVmsRamProgressItems(topFiveRam ?? []))
</script>

View File

@@ -3,7 +3,7 @@ import type { FrontXoPool } from '@/modules/pool/remote-resources/use-xo-pool-co
import type { useXoServerCollection } from '@/modules/server/remote-resources/use-xo-server-collection.ts'
import { createPool } from '@/test/create-pool.ts'
import { createServer } from '@/test/create-server.ts'
import { findCopiedValues } from '@/test/find-labelled-values.ts'
import { findCopiedValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -1,7 +1,7 @@
import PoolSystemGeneralInfo from '@/modules/pool/components/system/PoolSystemGeneralInfo.vue'
import type { FrontXoPool } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
import { createPool } from '@/test/create-pool.ts'
import { findCopiedValues } from '@/test/find-labelled-values.ts'
import { findCopiedValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -88,38 +88,14 @@ export function getStoragesUsageTotals(storagesUsage: StorageUsage[]): { totalUs
)
}
export function buildPercentProgressItems(usages: PercentUsage[]): ProgressBarGroupItem[] {
return usages.map(
usage =>
({
id: usage.id,
label: usage.name_label,
current: usage.percent,
total: 100,
}) satisfies ProgressBarGroupItem
)
export function toPercentProgressItem(usage: PercentUsage): ProgressBarGroupItem {
return { id: usage.id, label: usage.name_label, current: usage.percent, total: 100 }
}
export function buildHostsRamProgressItems(usages: HostRamUsage[]): ProgressBarGroupItem[] {
return usages.map(
usage =>
({
id: usage.id,
label: usage.name_label,
current: usage.usage,
total: usage.size,
}) satisfies ProgressBarGroupItem
)
export function toHostRamProgressItem(usage: HostRamUsage): ProgressBarGroupItem {
return { id: usage.id, label: usage.name_label, current: usage.usage, total: usage.size }
}
export function buildVmsRamProgressItems(usages: VmRamUsage[]): ProgressBarGroupItem[] {
return usages.map(
usage =>
({
id: usage.id,
label: usage.name_label,
current: usage.memory - usage.memoryFree,
total: usage.memory,
}) satisfies ProgressBarGroupItem
)
export function toVmRamProgressItem(usage: VmRamUsage): ProgressBarGroupItem {
return { id: usage.id, label: usage.name_label, current: usage.memory - usage.memoryFree, total: usage.memory }
}

View File

@@ -3,11 +3,11 @@ import {
buildStackedCpuUsageSeries,
buildStackedNetworkUsageSeries,
buildStackedRamUsageSeries,
buildHostsRamProgressItems,
buildPercentProgressItems,
buildVmsRamProgressItems,
getStackedRamUsageMaxValue,
getStoragesUsageTotals,
toHostRamProgressItem,
toPercentProgressItem,
toVmRamProgressItem,
} from '@/modules/pool/utils/xo-pool-dashboard.util.ts'
import { ONE_GB } from '@/shared/constants.ts'
import { createPoolStats } from '@/test/create-pool-stats.ts'
@@ -243,62 +243,50 @@ describe('getStoragesUsageTotals', () => {
})
})
describe('buildPercentProgressItems', () => {
it('maps each usage to a progress bar item out of 100', () => {
const storages = [
createStorageUsage({ id: 'sr-1' as StorageUsage['id'], name_label: 'Local storage', percent: 30 }),
createStorageUsage({ id: 'sr-2' as StorageUsage['id'], name_label: 'Shared storage', percent: 70 }),
]
describe('toPercentProgressItem', () => {
it('maps a usage to a progress bar item out of 100', () => {
const storage = createStorageUsage({ id: 'sr-1' as StorageUsage['id'], name_label: 'Local storage', percent: 30 })
expect(buildPercentProgressItems(storages)).toEqual([
{ id: 'sr-1', label: 'Local storage', current: 30, total: 100 },
{ id: 'sr-2', label: 'Shared storage', current: 70, total: 100 },
])
expect(toPercentProgressItem(storage)).toEqual({ id: 'sr-1', label: 'Local storage', current: 30, total: 100 })
})
it('keeps a fractional share as it is reported', () => {
expect(buildPercentProgressItems([{ id: 'host-1', name_label: 'Host 1', percent: 12.5 }])).toEqual([
{ id: 'host-1', label: 'Host 1', current: 12.5, total: 100 },
])
})
it('returns an empty array for an empty input', () => {
expect(buildPercentProgressItems([])).toEqual([])
expect(toPercentProgressItem({ id: 'host-1', name_label: 'Host 1', percent: 12.5 })).toEqual({
id: 'host-1',
label: 'Host 1',
current: 12.5,
total: 100,
})
})
})
describe('buildHostsRamProgressItems', () => {
it('maps each host to the memory it uses out of the memory it holds', () => {
const usages = [
{ id: 'host-1', name_label: 'Host 1', usage: 400, size: 1000 },
{ id: 'host-2', name_label: 'Host 2', usage: 750, size: 1000 },
]
expect(buildHostsRamProgressItems(usages)).toEqual([
{ id: 'host-1', label: 'Host 1', current: 400, total: 1000 },
{ id: 'host-2', label: 'Host 2', current: 750, total: 1000 },
])
})
it('returns an empty array for an empty input', () => {
expect(buildHostsRamProgressItems([])).toEqual([])
describe('toHostRamProgressItem', () => {
it('maps a host to the memory it uses out of the memory it holds', () => {
expect(toHostRamProgressItem({ id: 'host-1', name_label: 'Host 1', usage: 400, size: 1000 })).toEqual({
id: 'host-1',
label: 'Host 1',
current: 400,
total: 1000,
})
})
})
describe('buildVmsRamProgressItems', () => {
describe('toVmRamProgressItem', () => {
it('derives the memory a VM uses by taking its free memory off its total', () => {
const usages = [{ id: 'vm-1', name_label: 'VM 1', memory: 1000, memoryFree: 400 }]
expect(buildVmsRamProgressItems(usages)).toEqual([{ id: 'vm-1', label: 'VM 1', current: 600, total: 1000 }])
expect(toVmRamProgressItem({ id: 'vm-1', name_label: 'VM 1', memory: 1000, memoryFree: 400 })).toEqual({
id: 'vm-1',
label: 'VM 1',
current: 600,
total: 1000,
})
})
it('reports no memory used for a VM whose memory is entirely free', () => {
const usages = [{ id: 'vm-1', name_label: 'VM 1', memory: 1000, memoryFree: 1000 }]
expect(buildVmsRamProgressItems(usages)).toEqual([{ id: 'vm-1', label: 'VM 1', current: 0, total: 1000 }])
})
it('returns an empty array for an empty input', () => {
expect(buildVmsRamProgressItems([])).toEqual([])
expect(toVmRamProgressItem({ id: 'vm-1', name_label: 'VM 1', memory: 1000, memoryFree: 1000 })).toEqual({
id: 'vm-1',
label: 'VM 1',
current: 0,
total: 1000,
})
})
})

View File

@@ -5,7 +5,7 @@ import type { useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-
import type { XoVmDashboard } from '@/modules/vm/types/vm-dashboard.type.ts'
import { createSr } from '@/test/create-sr.ts'
import { createVm } from '@/test/create-vm.ts'
import { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { objectIcon } from '@core/icons'

View File

@@ -7,8 +7,7 @@ import type { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
import { createHost } from '@/test/create-host.ts'
import { createPool } from '@/test/create-pool.ts'
import { createVm } from '@/test/create-vm.ts'
import { findCopiedValues, findLabelledValues } from '@/test/find-labelled-values.ts'
import { findTags } from '@/test/find-tags.ts'
import { findCopiedValues, findLabelledValues, findTags } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import type { XoUser } from '@vates/types'

View File

@@ -11,8 +11,7 @@ import type { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
import { createHost } from '@/test/create-host.ts'
import { createPool } from '@/test/create-pool.ts'
import { createVm } from '@/test/create-vm.ts'
import { findCardLabelledValues } from '@/test/find-labelled-values.ts'
import { findTags } from '@/test/find-tags.ts'
import { findCardLabelledValues, findTags } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import type { XoUser } from '@vates/types'

View File

@@ -5,7 +5,7 @@ import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collecti
import { createVbd } from '@/test/create-vbd.ts'
import { createVdi } from '@/test/create-vdi.ts'
import { createVm } from '@/test/create-vm.ts'
import { findCardLabelledValues } from '@/test/find-labelled-values.ts'
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -2,7 +2,7 @@ import type { FrontXoVmSnapshot } from '@/modules/snapshot/components/remote-res
import type { useXo5VmSnapshotRoute } from '@/modules/snapshot/composables/xo-vm-snapshot-route-xo5.composable.ts'
import VmSnapshotCard from '@/modules/vm/components/snapshot/cards/VmSnapshotCard.vue'
import { createVmSnapshot } from '@/test/create-vm-snapshot.ts'
import { findCopiedValues, findLabelledValues } from '@/test/find-labelled-values.ts'
import { findCopiedValues, findLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { formatDateTime } from '@core/utils/time.util.ts'

View File

@@ -1,8 +1,7 @@
import VmSystemGeneralInfo from '@/modules/vm/components/system/VmSystemGeneralInfo.vue'
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import { createVm } from '@/test/create-vm.ts'
import { findCopiedValues, findLabelledValues } from '@/test/find-labelled-values.ts'
import { findTags } from '@/test/find-tags.ts'
import { findCopiedValues, findLabelledValues, findTags } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { VM_POWER_STATE } from '@vates/types'

View File

@@ -3,7 +3,7 @@ import VmSystemManagement from '@/modules/vm/components/system/VmSystemManagemen
import type { FrontXoVm } 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 { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { VM_OPERATIONS } from '@vates/types'

View File

@@ -1,7 +1,7 @@
import VmSystemNetworking from '@/modules/vm/components/system/VmSystemNetworking.vue'
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import { createVm } from '@/test/create-vm.ts'
import { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -1,7 +1,7 @@
import VmSystemResource from '@/modules/vm/components/system/VmSystemResource.vue'
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import { createVm } from '@/test/create-vm.ts'
import { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -1,7 +1,7 @@
import VmSystemStorageConfig from '@/modules/vm/components/system/VmSystemStorageConfig.vue'
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import { createVm } from '@/test/create-vm.ts'
import { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -1,7 +1,7 @@
import VmSystemVirtualizationAndBoot from '@/modules/vm/components/system/VmSystemVirtualizationAndBoot.vue'
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import { createVm } from '@/test/create-vm.ts'
import { findLabelledValues } from '@/test/find-labelled-values.ts'
import { findLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { mount } from '@vue/test-utils'

View File

@@ -0,0 +1,22 @@
import type { XoPoolDashboard } from '@/modules/pool/types/xo-pool-dashboard.type.ts'
type HostsTopFiveUsage = NonNullable<NonNullable<XoPoolDashboard['hosts']>['topFiveUsage']>
type VmsTopFiveUsage = NonNullable<NonNullable<XoPoolDashboard['vms']>['topFiveUsage']>
/**
* Builds an `XoPoolDashboard` carrying a top-five usage section for the hosts
* and for the VMs.
*
* Both metrics of a section default to empty, so a case fills in only the one
* it exercises: each card reads `cpu` or `ram`, never both.
*/
export function createPoolDashboardTopFiveUsage(
hosts: Partial<HostsTopFiveUsage> = {},
vms: Partial<VmsTopFiveUsage> = {}
): XoPoolDashboard {
return {
hosts: { topFiveUsage: { cpu: [], ram: [], ...hosts } },
vms: { topFiveUsage: { cpu: [], ram: [], ...vms } },
}
}

View File

@@ -3,6 +3,21 @@ import type { VueWrapper } from '@vue/test-utils'
type QueryableWrapper = Pick<VueWrapper, 'findAll'>
type PairSelectors = { root: string; label: string; value: string }
const KEY_VALUE_ROW_SELECTOR = '.vts-tabular-key-value-row, .vts-key-value-row'
const CARD_KEY_VALUE_ROW_SELECTOR = '.vts-card-row-key-value'
/**
* The reading strategy every helper below shares. Selectors travel in an object
* rather than positionally: transposing `label` and `value` would still
* typecheck and still produce plausible-looking output.
*/
function findPairs(wrapper: QueryableWrapper, { root, label, value }: PairSelectors): [string, string][] {
return wrapper.findAll(root).map(element => [element.get(label).text(), element.get(value).text()])
}
/**
* Reads every `VtsTabularKeyValueRow` / `VtsKeyValueRow` of a mounted component
* as a plain `{ label: value }` record, so a single assertion can cover all the
@@ -11,9 +26,7 @@ type QueryableWrapper = Pick<VueWrapper, 'findAll'>
* Rows sharing the same label collapse into one entry: query those directly.
*/
export function findLabelledValues(wrapper: QueryableWrapper): Record<string, string> {
const rows = wrapper.findAll('.vts-tabular-key-value-row, .vts-key-value-row')
return Object.fromEntries(rows.map(row => [row.get('dt.label').text(), row.get('dd.value').text()]))
return Object.fromEntries(findPairs(wrapper, { root: KEY_VALUE_ROW_SELECTOR, label: 'dt.label', value: 'dd.value' }))
}
/**
@@ -21,9 +34,7 @@ export function findLabelledValues(wrapper: QueryableWrapper): Record<string, st
* the side-panel cards.
*/
export function findCardLabelledValues(wrapper: QueryableWrapper): Record<string, string> {
const rows = wrapper.findAll('.vts-card-row-key-value')
return Object.fromEntries(rows.map(row => [row.get('.key').text(), row.get('.value').text()]))
return Object.fromEntries(findPairs(wrapper, { root: CARD_KEY_VALUE_ROW_SELECTOR, label: '.key', value: '.value' }))
}
/**
@@ -33,7 +44,7 @@ export function findCardLabelledValues(wrapper: QueryableWrapper): Record<string
*/
export function findCopiedValues(wrapper: QueryableWrapper): Record<string, unknown> {
return Object.fromEntries(
wrapper.findAll('.vts-tabular-key-value-row, .vts-key-value-row').flatMap(row => {
wrapper.findAll(KEY_VALUE_ROW_SELECTOR).flatMap(row => {
const button = row.findComponent(VtsCopyButton)
return button.exists() ? [[row.get('dt.label').text(), button.props('value')]] : []
@@ -50,7 +61,7 @@ export function findCopiedValues(wrapper: QueryableWrapper): Record<string, unkn
*/
export function findCardCopiedValues(wrapper: QueryableWrapper): unknown[] {
return wrapper
.findAll('.vts-card-row-key-value')
.findAll(CARD_KEY_VALUE_ROW_SELECTOR)
.flatMap(row => row.findAllComponents(VtsCopyButton))
.map(button => button.props('value'))
}
@@ -63,9 +74,18 @@ export function findCardCopiedValues(wrapper: QueryableWrapper): unknown[] {
* order it lists them in is part of what a user reads.
*/
export function findLegends(wrapper: QueryableWrapper): [string, string][] {
const legends = wrapper.findAll('.ui-legend')
return findPairs(wrapper, { root: '.ui-legend', label: '.label', value: '.value-and-unit' })
}
return legends.map(legend => [legend.get('.label').text(), legend.get('.value-and-unit').text()])
/**
* Same as {@link findLegends}, but grouped by `VtsProgressBarGroup`: a card that
* renders several groups needs to know which group a legend landed in, where a
* flat list of legends would not say.
*/
export function findProgressBarGroupLegends(wrapper: QueryableWrapper): [string, string][][] {
const sections = wrapper.findAll('.vts-progress-bar-group')
return sections.map(section => findLegends(section))
}
/**
@@ -74,7 +94,28 @@ export function findLegends(wrapper: QueryableWrapper): [string, string][] {
* card passes a `max`, the percentage above it.
*/
export function findCardNumbers(wrapper: QueryableWrapper): [string, string][] {
const cards = wrapper.findAll('.ui-card-numbers')
return cards.map(card => [card.get('.label').text(), card.get('.values').text()])
return findPairs(wrapper, { root: '.ui-card-numbers', label: '.label', value: '.values' })
}
/**
* Reads the `tbody` of a mounted table as one array of cell texts per row.
*
* Cells rather than each row's whole text: `row.text()` concatenates the
* columns, so a patch named `XSAPATCH-1` at version `1.0` and one named
* `XSAPATCH-11` at version `.0` would read the same.
*/
export function findTableRows(wrapper: QueryableWrapper): string[][] {
const rows = wrapper.findAll('tbody tr')
return rows.map(row => row.findAll('td').map(cell => cell.text()))
}
/**
* Reads every `UiTag` of a mounted component as its text, in the order the
* component lists them.
*/
export function findTags(wrapper: QueryableWrapper): string[] {
const tags = wrapper.findAll('.ui-tag')
return tags.map(tag => tag.text())
}

View File

@@ -1,5 +0,0 @@
import type { VueWrapper } from '@vue/test-utils'
export function findTags(wrapper: Pick<VueWrapper, 'findAll'>) {
return wrapper.findAll('.ui-tag').map(tag => tag.text())
}

View File

@@ -18,6 +18,7 @@
- [Backup jobs] Add backup job list in admin menu (PR [#10630](https://github.com/vatesfr/xen-orchestra/pull/10630))
- [Backup jobs] Add backup job side panel in admin menu (PR [#10631](https://github.com/vatesfr/xen-orchestra/pull/10631))
- [Backup jobs] The backup links of the site dashboard and the VM Backups tab now use the backup jobs of the admin menu (PR [#10636](https://github.com/vatesfr/xen-orchestra/pull/10636))
- [Pool/Dashboard] In the CPU and RAM usage cards, show a "No data to calculate" message when there is no host or VM to rank (PR [#10386](https://github.com/vatesfr/xen-orchestra/pull/10386))
### Bug fixes