refacto for merge

This commit is contained in:
Marie Garde
2026-11-09 10:38:44 +01:00
parent c8e0db8730
commit 72f433b442
14 changed files with 198 additions and 65 deletions

View File

@@ -46,7 +46,13 @@
</div>
</div>
<template v-if="hasSubTasks && expanded">
<UiTaskList :tasks="subTasks" :depth :selected-task-id="selectedTaskId" @select="id => emit('select', id)" />
<UiTaskList
:tasks="subTasks"
:depth
:selected-task-id="selectedTaskId"
:auto-collapse-completed
@select="id => emit('select', id)"
/>
</template>
</li>
</template>
@@ -96,6 +102,7 @@ const { task, showEyeIcon = false } = defineProps<{
selected?: boolean
selectedTaskId?: string
showEyeIcon?: boolean
autoCollapseCompleted?: boolean
}>()
const emit = defineEmits<{

View File

@@ -9,6 +9,7 @@
:depth="depth + 1"
:selected="selectedTaskId === task.id"
:selected-task-id="selectedTaskId"
:auto-collapse-completed
@select="emit('select', $event)"
@expand="task.toggleFlag('expanded')"
/>
@@ -19,11 +20,17 @@
import UiTaskItem from '@core/components/ui/task-item/UiTaskItem.vue'
import type { Task } from '@core/components/ui/task-item/UiTaskItem.vue'
import { useCollection } from '@core/packages/collection'
import { watch } from 'vue'
const { tasks, depth = 0 } = defineProps<{
const {
tasks,
depth = 0,
autoCollapseCompleted,
} = defineProps<{
tasks: Task[]
depth?: number
selectedTaskId?: string
autoCollapseCompleted?: boolean
}>()
const emit = defineEmits<{
@@ -33,6 +40,16 @@ const emit = defineEmits<{
const { items: tasksItems } = useCollection(() => tasks, {
flags: { expanded: true },
})
watch(
() =>
autoCollapseCompleted ? tasksItems.value.filter(task => task.source.end !== undefined).map(task => task.id) : [],
(completedTaskIds, previousCompletedTaskIds = []) => {
tasksItems.value
.filter(task => completedTaskIds.includes(task.id) && !previousCompletedTaskIds.includes(task.id))
.forEach(task => task.toggleFlag('expanded', false))
}
)
</script>
<style scoped lang="postcss">

View File

@@ -226,6 +226,7 @@
"attach-vdi": "Attach existing VDI",
"attach-vdi:error-message": "An error occurred while attaching the VDI. Please review your configuration settings (SR, VDI, mode), verify the VM state, and try again.",
"attaching-vdi": "Attaching VDI",
"auto-collapse-completed-tasks": "Auto-collapse completed tasks",
"auto-generated": "Automatically generated",
"auto-power": "Auto power",
"available": "Available",
@@ -1497,6 +1498,7 @@
"vifs": "VIFs",
"vifs-must-be-reassigned": "VIFs must be reassigned.",
"viridian": "Viridian",
"virtual-size": "Virtual size",
"virtual-tpm": "Virtual TPM (VTPM)",
"virtualization-boot-settings": "Virtualization & boot settings",
"virtualization-mode": "Virtualization mode",

View File

@@ -226,6 +226,7 @@
"attach-vdi": "Attacher un VDI existant",
"attach-vdi:error-message": "Une erreur est survenue lors de l'attachement du VDI. Veuillez vérifier votre configuration (SR, VDI, mode), l'état de la VM, puis réessayer.",
"attaching-vdi": "Attachement du VDI en cours",
"auto-collapse-completed-tasks": "Replier automatiquement les tâches terminées",
"auto-generated": "Généré automatiquement",
"auto-power": "Démarrage automatique",
"available": "Disponible",
@@ -1497,6 +1498,7 @@
"vifs": "VIFs",
"vifs-must-be-reassigned": "Les VIFs doivent être réassignés.",
"viridian": "Viridian",
"virtual-size": "Taille virtuelle",
"virtual-tpm": "TPM virtuel (VTPM)",
"virtualization-boot-settings": "Virtualisation & paramètres de démarrage",
"virtualization-mode": "Mode de virtualisation",

View File

@@ -0,0 +1,13 @@
import { defineColumns } from '@core/packages/table/define-columns.ts'
import { useLinkColumn } from '@core/tables/column-definitions/link-column.ts'
import { useNumberColumn } from '@core/tables/column-definitions/number-column.ts'
import { useI18n } from 'vue-i18n'
export const useBackupLogVmColumns = defineColumns(() => {
const { t } = useI18n()
return {
vm: useLinkColumn({ headerLabel: () => t('vm') }),
virtualSize: useNumberColumn({ headerLabel: () => t('virtual-size') }),
}
})

View File

@@ -0,0 +1,99 @@
import BackupLogBackedUpVmsTable from '@/modules/backup-log/components/backed-up-vms/BackupLogBackedUpVmsTable.vue'
import type { FrontXoBackupLog } from '@/modules/backup-log/remote-resources/use-xo-backup-log-collection.ts'
import type { FrontXoVbd, useXoVbdCollection } from '@/modules/vbd/remote-resources/use-xo-vbd-collection.ts'
import type { FrontXoVdi, useXoVdiCollection } from '@/modules/vdi/remote-resources/use-xo-vdi-collection.ts'
import type { FrontXoVm, useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import { createBackupLog, createBackupLogVmTask } from '@/test/create-backup-log.ts'
import { createVbd } from '@/test/create-vbd.ts'
import { createVdi } from '@/test/create-vdi.ts'
import { createVm } from '@/test/create-vm.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import { formatSizeRaw } from '@core/utils/size.util.ts'
import { mount } from '@vue/test-utils'
const { vmsById, vbdsById, vdisById } = vi.hoisted(() => ({
vmsById: new Map<string, FrontXoVm>(),
vbdsById: new Map<string, FrontXoVbd>(),
vdisById: new Map<string, FrontXoVdi>(),
}))
vi.mock(import('@/modules/vm/remote-resources/use-xo-vm-collection.ts'), () => ({
useXoVmCollection: (() => ({
getVmById: (id: string) => vmsById.get(id),
})) as unknown as typeof useXoVmCollection,
}))
vi.mock(import('@/modules/vbd/remote-resources/use-xo-vbd-collection.ts'), () => ({
useXoVbdCollection: (() => ({
getVbdsByIds: (ids: string[]) => ids.flatMap(id => vbdsById.get(id) ?? []),
})) as unknown as typeof useXoVbdCollection,
}))
vi.mock(import('@/modules/vdi/remote-resources/use-xo-vdi-collection.ts'), () => ({
useXoVdiCollection: (() => ({
getVdiById: (id: string) => vdisById.get(id),
})) as unknown as typeof useXoVdiCollection,
}))
beforeEach(() => {
vmsById.clear()
vbdsById.clear()
vdisById.clear()
})
function mockVmWithDisks(disks: { vbd: Partial<FrontXoVbd>; vdi: Partial<FrontXoVdi> & Pick<FrontXoVdi, 'id'> }[]) {
const vbds = disks.map(({ vbd, vdi }, index) =>
createVbd({ id: `vbd-${index}` as FrontXoVbd['id'], VDI: vdi.id, ...vbd })
)
vbds.forEach(vbd => vbdsById.set(vbd.id, vbd))
disks.forEach(({ vdi }) => vdisById.set(vdi.id, createVdi(vdi)))
vmsById.set('vm-1', createVm({ id: 'vm-1' as FrontXoVm['id'], $VBDs: vbds.map(vbd => vbd.id) }))
}
function findVirtualSizeCell(backupLog: FrontXoBackupLog) {
const wrapper = mount(BackupLogBackedUpVmsTable, {
props: { backupLog },
global: createGlobalTestConfig(),
})
const headers = wrapper.findAll('thead th').map(header => header.text())
return wrapper.findAll('tbody tr td')[headers.indexOf(t('virtual-size'))]
}
describe('virtual size', () => {
it('sums the size of the VM disks', () => {
mockVmWithDisks([
{ vbd: {}, vdi: { id: 'vdi-1' as FrontXoVdi['id'], size: 1_073_741_824 } },
{ vbd: {}, vdi: { id: 'vdi-2' as FrontXoVdi['id'], size: 1_073_741_824 } },
])
const { value, prefix } = formatSizeRaw(2_147_483_648, 2)
const cellText = findVirtualSizeCell(createBackupLog({ tasks: [createBackupLogVmTask('vm-1')] })).text()
expect(cellText).toContain(String(value))
expect(cellText).toContain(prefix)
})
it('ignores the CD drives', () => {
mockVmWithDisks([
{ vbd: {}, vdi: { id: 'vdi-1' as FrontXoVdi['id'], size: 1_073_741_824 } },
{ vbd: { is_cd_drive: true }, vdi: { id: 'vdi-2' as FrontXoVdi['id'], size: 5_368_709_120 } },
])
const { value, prefix } = formatSizeRaw(1_073_741_824, 2)
const cellText = findVirtualSizeCell(createBackupLog({ tasks: [createBackupLogVmTask('vm-1')] })).text()
expect(cellText).toContain(String(value))
expect(cellText).toContain(prefix)
})
it('leaves the virtual size empty for a VM that no longer exists', () => {
expect(findVirtualSizeCell(createBackupLog({ tasks: [createBackupLogVmTask('vm-1')] })).text()).toBe('')
})
})

View File

@@ -20,10 +20,11 @@
</template>
<script setup lang="ts">
import { useXoBackupLogsUtils } from '@/modules/backup-log/composables/use-xo-backup-log-utils.composable.ts'
import type { FrontXoBackupLog } from '@/modules/backup-log/remote-resources/use-xo-backup-log-collection.ts'
import type { BackupLogVmFilterableData } from '@/modules/backup-log/types/xo-backup-log.type.ts'
import { getBackupLogVmTasks } from '@/modules/backup-log/utils/xo-backup-log.util.ts'
import { useXoVbdCollection } from '@/modules/vbd/remote-resources/use-xo-vbd-collection.ts'
import { type FrontXoVdi, useXoVdiCollection } from '@/modules/vdi/remote-resources/use-xo-vdi-collection.ts'
import { type FrontXoVm, useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import VtsQueryBuilder from '@core/components/query-builder/VtsQueryBuilder.vue'
import VtsRow from '@core/components/table/VtsRow.vue'
@@ -34,7 +35,7 @@ import { useTableState } from '@core/composables/table-state.composable.ts'
import { objectIcon } from '@core/icons'
import { useQueryBuilderSchema } from '@core/packages/query-builder/schema/use-query-builder-schema.ts'
import { useQueryBuilderFilter } from '@core/packages/query-builder/use-query-builder-filter.ts'
import { useVmColumns } from '@core/tables/column-sets/vm-columns.ts'
import { useBackupLogVmColumns } from '@core/tables/column-sets/backup-log-vm-columns.ts'
import { useStringSchema } from '@core/utils/query-builder/use-string-schema.ts'
import { formatSizeRaw } from '@core/utils/size.util.ts'
import { toLower } from 'lodash-es'
@@ -49,16 +50,29 @@ const { t } = useI18n()
const { getVmById } = useXoVmCollection()
const { findVmTransfer } = useXoBackupLogsUtils()
const { getVbdsByIds } = useXoVbdCollection()
const { getVdiById } = useXoVdiCollection()
function getVmVirtualSize(vm: FrontXoVm | undefined) {
if (vm === undefined) {
return undefined
}
return getVbdsByIds(vm.$VBDs)
.filter(vbd => !vbd.is_cd_drive)
.reduce((totalSize, vbd) => totalSize + (getVdiById(vbd.VDI as FrontXoVdi['id'])?.size ?? 0), 0)
}
const vms = computed(() =>
getBackupLogVmTasks(backupLog).map(vmTask => {
const id = vmTask.data?.id as FrontXoVm['id']
const vm = getVmById(id)
return {
id,
name: getVmById(id)?.name_label ?? vmTask.data?.name_label ?? id,
size: findVmTransfer(vmTask)?.size,
name: vm?.name_label ?? vmTask.data?.name_label ?? id,
size: getVmVirtualSize(vm),
}
})
)
@@ -80,8 +94,7 @@ const state = useTableState({
vms.value.length === 0 ? t('no-vm-detected') : filteredVms.value.length === 0 ? { type: 'no-result' } : false,
})
const { HeadCells, BodyCells } = useVmColumns({
exclude: ['checkbox', 'ipAddresses', 'vcpus', 'ram', 'tags', 'actions'],
const { HeadCells, BodyCells } = useBackupLogVmColumns({
body: (vm: BackupLogVmFilterableData) => {
const xoVm = computed(() => getVmById(vm.id))
@@ -96,7 +109,7 @@ const { HeadCells, BodyCells } = useVmColumns({
icon: objectIcon('vm', toLower(xoVm.value.power_state)),
})
: r({ label: vm.name }),
diskSpace: r => r(size.value?.value, size.value?.prefix),
virtualSize: r => r(size.value?.value, size.value?.prefix),
}
},
})

View File

@@ -20,7 +20,7 @@
<VtsTabularKeyValueRow :label="t('transfer-size')" :value="transferSize" />
<VtsTabularKeyValueRow :label="t('task')">
<template v-if="task !== undefined" #value>
<UiLink size="medium" :to="getTaskRoute(task.id)" icon="fa:bars-progress">
<UiLink size="medium" :to="taskRoute" icon="fa:bars-progress">
{{ task.properties.name ?? task.id }}
</UiLink>
</template>
@@ -66,4 +66,6 @@ const transferSize = computed(() => {
})
const task = computed(() => tasks.value.find(task => task.properties.runJobId === backupLog.id))
const taskRoute = computed(() => (task.value !== undefined ? getTaskRoute(task.value.id) : undefined))
</script>

View File

@@ -1,12 +1,6 @@
<template>
<UiPanelCard class="backup-log-task-quick-infos-card">
<UiPanelCardTitle
:id="task.id"
size="medium"
:label="taskName.name"
:to="taskName.to"
icon="fa:bars-progress"
/>
<UiPanelCardTitle :id="task.id" size="medium" :label="taskName.name" :to="taskName.to" icon="fa:bars-progress" />
<div class="content">
<VtsCardRowKeyValue v-if="type !== undefined">
<template #key>{{ t('type') }}</template>
@@ -143,11 +137,13 @@ const taskAccent = computed(() => getTaskAccents(task.status).statusAccent)
const progressAccent = computed(() => getTaskAccents(task.status).progressAccent)
const progress = computed(() => {
const dataProgress = typeof task.data?.progress === 'number' ? task.data.progress : undefined
if (task.status === 'pending' && !task.end) {
return task.progress ?? 0
return dataProgress ?? 0
}
return task.progress ?? 100
return dataProgress ?? 100
})
</script>

View File

@@ -15,6 +15,7 @@ export function useXoBackupLogTasksConversion(rawBackupLog: MaybeRefOrGetter<Fro
id: task.id,
status: task.status === 'interrupted' ? 'failure' : task.status,
...getTaskName(task),
progress: typeof task.data?.progress === 'number' ? task.data.progress : 0,
start: task.start,
end: task.end,
infos: task.infos,

View File

@@ -35,6 +35,7 @@ describe('useXoBackupLogTasksConversion', () => {
id: 'task-1',
status: 'success',
name: 'name of backup VM',
progress: 0,
start: 1_000,
end: 2_000,
infos,
@@ -60,6 +61,16 @@ describe('useXoBackupLogTasksConversion', () => {
expect(vmTask.subtasks?.[0].subtasks?.[0].name).toBe('name of transfer')
})
it('gives the progress the backup sets in the task data', () => {
const data = { progress: 42 } as unknown as BackupLogTask['data']
const backupLog = createBackupLog({
tasks: [createBackupLogTask({ message: 'read disk', status: 'pending', data })],
})
expect(mountTasksConversion(backupLog).convertedTasks[0].progress).toBe(42)
})
it('shows an interrupted task as failed', () => {
const backupLog = createBackupLog({ tasks: [createBackupLogTask({ status: 'interrupted' })] })

View File

@@ -2,6 +2,11 @@
<div class="tasks-list">
<UiTitle>
{{ title ?? t('tasks') }}
<template v-if="autoCollapseToggle" #action>
<UiToggle v-model="autoCollapseCompleted" class="auto-collapse-toggle">
{{ t('auto-collapse-completed-tasks') }}
</UiToggle>
</template>
</UiTitle>
<UiQuerySearchBar @search="value => (searchQuery = value)" />
<div class="container">
@@ -19,6 +24,7 @@
v-else
:tasks="tasksRecords"
:selected-task-id="selectedTaskId"
:auto-collapse-completed
@select="id => (selectedTaskId = id)"
/>
<VtsStateHero v-if="searchQuery && filteredTasks.length === 0" format="table" type="no-result" size="small">
@@ -36,6 +42,7 @@ import UiQuerySearchBar from '@core/components/ui/query-search-bar/UiQuerySearch
import UiTableControlsBar from '@core/components/ui/table-controls-bar/UiTableControlsBar.vue'
import UiTaskList from '@core/components/ui/task-list/UiTaskList.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import UiToggle from '@core/components/ui/toggle/UiToggle.vue'
import { usePagination } from '@core/composables/pagination.composable.ts'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
import { computed, ref } from 'vue'
@@ -46,6 +53,7 @@ const { tasks, title } = defineProps<{
hasError: boolean
busy: boolean
title?: string
autoCollapseToggle?: boolean
}>()
const { t } = useI18n()
@@ -54,6 +62,8 @@ const selectedTaskId = useRouteQuery('id')
const searchQuery = ref('')
const autoCollapseCompleted = ref(false)
const filteredTasks = computed(() => {
const searchTerm = searchQuery.value.trim().toLocaleLowerCase()
@@ -82,5 +92,9 @@ const { pageRecords: tasksRecords, paginationBindings } = usePagination('tasks',
.actions {
gap: 0.8rem;
}
.auto-collapse-toggle {
text-transform: none;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<VtsContentSidePanel class="run">
<UiCard class="container">
<TasksList :tasks="convertedTasks" :title="t('run')" :has-error="false" :busy="false" />
<TasksList :tasks="convertedTasks" :title="t('run')" :has-error="false" :busy="false" auto-collapse-toggle />
</UiCard>
<BackupLogTaskSidePanel :task="selectedTask" @close="selectedTask = undefined" />
</VtsContentSidePanel>

View File

@@ -1,44 +0,0 @@
### Description
Adds a detail page for backup logs in Admin › Backup & Replication › Logs, reachable from the title of the log side panel. It has 3 tabs:
- **General**: general information and schedule of the run, with a link to configure the job in XO 5
- **Run**: tree of the run tasks; selecting a task opens a side panel with its information, warnings, errors and JSON (like "copy log" in XO 5)
- **Backed up VMs**: backed up VMs with the size transferred by this run
### Notes for reviewers
- Horizontal tabs, like the other detail pages: the vertical tabs of the mockup do not exist in the design system
- The Run tab reuses `TasksList`, which gets a `title` prop
- No progress percentage on running tasks: the backend only sends a real one for incremental XAPI backups, per disk and on the transfer tasks, design is being asked
- The log side panel title now links to the detail page instead of XO 5
- New `computeTransferSpeed` util, shared by the log transfer speed and the VM transfer speed
- New i18n keys in web-core: `end-date`, `json`, `merge`, `start-date`, `transfer`, `transfer-size`
### Screenshots
| | Light | Dark |
| ----------------------- | ----- | ---- |
| General tab | | |
| Run tab | | |
| Run tab with side panel | | |
| Backed up VMs tab | | |
### Checklist
- Commit
- Title follows [commit conventions](https://bit.ly/commit-conventions)
- Reference the relevant issue, forum thread, discussion, ... (`Fixes #007`, `See xoa-support#42`, `See https://...`)
- If bug fix, add `Introduced by <commit|PR>`
- Changelog
- If visible by XOA users, add changelog entry to `CHANGELOG.unreleased.md`
- If visible by XO Lite users, add changelog entry to `@xen-orchestra/lite/CHANGELOG.md`
- Update "Packages to release" in `CHANGELOG.unreleased.md`
- PR
- If UI changes, add a `### Screenshots` section above this checklist
- If not finished or not tested, open as _Draft_
### Review process
- External contributors: simply open the pull request, we'll get back to you as soon as possible.
- Vates developers: follow the internal pull request process.