feat(xo6): Add backup lob detail page

This commit is contained in:
Marie Garde
2026-11-06 09:02:57 +01:00
parent c8c11ae63c
commit 3f1604c955
30 changed files with 976 additions and 22 deletions

View File

@@ -1,12 +1,12 @@
<template>
<span v-if="task.nameParts || task.name" class="vts-task-name">
<UiLink v-if="task.to" :size display-inline :to="task.to">{{ fullName }}</UiLink>
<UiLink v-if="task.to" :size :display-inline="!task.icon" :to="task.to" :icon="task.icon">{{ fullName }}</UiLink>
<template v-else-if="task.nameParts">
<UiLink v-for="(part, index) in task.nameParts" :key="index" :size display-inline :to="part.to">{{
part.text
}}</UiLink>
</template>
<UiLink v-else :size display-inline>{{ task.name }}</UiLink>
<UiLink v-else :size :display-inline="!task.icon" :icon="task.icon">{{ task.name }}</UiLink>
</span>
</template>
@@ -16,7 +16,7 @@ import type { Task } from '@core/components/ui/task-item/UiTaskItem.vue'
import { computed } from 'vue'
const { task, size } = defineProps<{
task: Pick<Task, 'name' | 'nameParts' | 'to'>
task: Pick<Task, 'name' | 'icon' | 'nameParts' | 'to'>
size: 'small' | 'medium'
}>()

View File

@@ -61,6 +61,7 @@ import UiTaskList from '@core/components/ui/task-list/UiTaskList.vue'
import type { LinkOptions } from '@core/composables/link-component.composable.ts'
import { useTimeAgo } from '@core/composables/locale-time-ago.composable.ts'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import type { IconName } from '@core/icons'
import { logicOr } from '@vueuse/math'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -77,6 +78,7 @@ export type Task = {
id: string
infos?: { data: unknown; message: string }[]
name?: string
icon?: IconName
nameParts?: TaskObjectSegment[]
to?: LinkOptions['to']
progress?: number

View File

@@ -435,6 +435,7 @@
"encryption-available-for-block-modes-only": "Encryption is available for block modes only.",
"encryption-key-invalid": "The key must contain exactly {n} hexadecimal characters.",
"encryption-key-loss-warning": "If you lose the encryption key, your data cannot be recovered.",
"end-date": "End date",
"ended": "Ended",
"endpoint-url": "Endpoint URL",
"engines-off": "Engines off, orbit stable",
@@ -767,6 +768,7 @@
"job:vm-suspend:in-progress": "Suspend in progress…",
"job:vm-suspend:missing-vm": "No VMs to suspend",
"jobs": "Jobs",
"json": "JSON",
"json-schema:hide-examples": "Hide examples",
"json-schema:nested-label": "{parent} › {label}",
"json-schema:numbered-item": "{label} {n}",
@@ -824,6 +826,7 @@
"maximum-vcpu": "Maximum vCPUs",
"memory": "Memory",
"memory-usage": "Memory usage",
"merge": "Merge",
"merge-backups-synchronously": "Merge backups synchronously",
"message": "Message",
"migration-blocked": "Migration Blocked",
@@ -1293,6 +1296,7 @@
"stacked-cpu-usage": "Stacked CPU usage",
"stacked-ram-usage": "Stacked RAM usage",
"start-console": "Start your {type} to access the console and resume operations!",
"start-date": "Start date",
"start-delay": "Start delay",
"start-host": "Start the host to resume operations, or check back later once it’s powered on.",
"start-vm": "Start it to access its console, and configuration details.",
@@ -1404,6 +1408,8 @@
"traffic-rules:rule-ordinal-suffixes": "one:st, two:nd, few:rd, other:th",
"traffic-rules:target-on": "@.capitalize:direction-on",
"traffic-rules:xapi-plugin-required": "Requires XAPI plugin mode",
"transfer": "Transfer",
"transfer-size": "@:transfer @.lower:size",
"translation-tool": "Translation tool",
"treeview": "Treeview",
"trigger": "Trigger",

View File

@@ -435,6 +435,7 @@
"encryption-available-for-block-modes-only": "Le cryptage est disponible uniquement pour les modes par blocs.",
"encryption-key-invalid": "La clé doit contenir exactement {n} caractères hexadécimaux.",
"encryption-key-loss-warning": "Si vous perdez la clé de cryptage, vos données ne peuvent pas être récupérées.",
"end-date": "Date de fin",
"ended": "Terminé",
"endpoint-url": "URL du endpoint",
"engines-off": "Moteurs arrêtés, orbite stable",
@@ -767,6 +768,7 @@
"job:vm-suspend:in-progress": "Suspension en cours…",
"job:vm-suspend:missing-vm": "Aucune VM à suspendre",
"jobs": "Jobs",
"json": "JSON",
"json-schema:hide-examples": "Masquer les exemples",
"json-schema:nested-label": "{parent} › {label}",
"json-schema:numbered-item": "{label} {n}",
@@ -824,6 +826,7 @@
"maximum-vcpu": "Nombre maximal de vCPU",
"memory": "Mémoire",
"memory-usage": "Utilisation de la mémoire",
"merge": "Fusion",
"merge-backups-synchronously": "Fusionner les sauvegardes de manière synchrone",
"message": "Message",
"migration-blocked": "Migration bloquée",
@@ -1293,6 +1296,7 @@
"stacked-cpu-usage": "Utilisation CPU empilée",
"stacked-ram-usage": "Utilisation RAM empilée",
"start-console": "Démarrez votre {type} pour accéder à la console et reprendre les opérations !",
"start-date": "Date de début",
"start-delay": "Départ différé",
"start-host": "Démarrez l’hôte pour reprendre les opérations, ou revenez plus tard une fois qu’il sera allumé.",
"start-vm": "Démarrez-la pour accéder à sa console et à ses paramètres de configuration.",
@@ -1404,6 +1408,8 @@
"traffic-rules:rule-ordinal-suffixes": "one:re, other:e",
"traffic-rules:target-on": "@.capitalize:direction-on",
"traffic-rules:xapi-plugin-required": "Nécessite le mode plugin XAPI",
"transfer": "Transfert",
"transfer-size": "@:size du @.lower:transfer",
"translation-tool": "Outil de traduction",
"treeview": "Arborescence",
"trigger": "Déclencheur",

View File

@@ -0,0 +1,65 @@
<template>
<BackupLogHeaderBreadcrumb :log-name />
<UiHeadBar icon="object:backup-run">
{{ logName }}
<template #actions>
<UiLink size="medium" :href="xo5LogsHref">
{{ t('configure-in-xo-5') }}
</UiLink>
</template>
</UiHeadBar>
<TabList>
<RouterLink
v-slot="{ isActive, href }"
:to="{ name: '/admin/backup-log/[id]/general', params: { id: backupLog.id } }"
custom
>
<UiTabItem :active="isActive" :href tag="a">
{{ t('general') }}
</UiTabItem>
</RouterLink>
<RouterLink
v-slot="{ isActive, href }"
:to="{ name: '/admin/backup-log/[id]/run', params: { id: backupLog.id } }"
custom
>
<UiTabItem :active="isActive" :href tag="a">
{{ t('run') }}
</UiTabItem>
</RouterLink>
<RouterLink
v-slot="{ isActive, href }"
:to="{ name: '/admin/backup-log/[id]/backed-up-vms', params: { id: backupLog.id } }"
custom
>
<UiTabItem :active="isActive" :href tag="a">
{{ t('backed-up-vms') }}
</UiTabItem>
</RouterLink>
</TabList>
</template>
<script setup lang="ts">
import BackupLogHeaderBreadcrumb from '@/modules/backup-log/components/header/BackupLogHeaderBreadcrumb.vue'
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 { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
import TabList from '@core/components/tab-list/TabList.vue'
import UiHeadBar from '@core/components/ui/head-bar/UiHeadBar.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiTabItem from '@core/components/ui/tab-item/UiTabItem.vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { backupLog } = defineProps<{ backupLog: FrontXoBackupLog }>()
const { t } = useI18n()
const { getBackupLogDate } = useXoBackupLogsUtils()
const logName = computed(() => t('log-name', { date: getBackupLogDate(backupLog.start) }))
const { buildXo5Route } = useXoRoutes()
const xo5LogsHref = computed(() => buildXo5Route('/backup/overview'))
</script>

View File

@@ -0,0 +1,104 @@
<template>
<div class="backup-log-backed-up-vms-table">
<UiTitle>
{{ t('backed-up-vms') }}
</UiTitle>
<VtsQueryBuilder :model-value="filter" :schema @search="filter = $event" />
<VtsTable :state :pagination-bindings>
<thead>
<tr>
<HeadCells />
</tr>
</thead>
<tbody>
<VtsRow v-for="vm of paginatedVms" :key="vm.id">
<BodyCells :item="vm" />
</VtsRow>
</tbody>
</VtsTable>
</div>
</template>
<script setup lang="ts">
import { useXoBackupLogTaskName } from '@/modules/backup-log/composables/use-xo-backup-log-task-name.composable.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 type { FrontXoVm } 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'
import VtsTable from '@core/components/table/VtsTable.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { usePagination } from '@core/composables/pagination.composable.ts'
import { useTableState } from '@core/composables/table-state.composable.ts'
import { 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 { useStringSchema } from '@core/utils/query-builder/use-string-schema.ts'
import { formatSizeRaw } from '@core/utils/size.util.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { backupLog } = defineProps<{
backupLog: FrontXoBackupLog
}>()
const { t } = useI18n()
const { getTaskName } = useXoBackupLogTaskName()
const { findVmTransfer } = useXoBackupLogsUtils()
const vms = computed(() =>
getBackupLogVmTasks(backupLog).map(vmTask => {
const id = vmTask.data?.id as FrontXoVm['id']
const { name, icon, to } = getTaskName(vmTask)
return {
id,
name: name ?? id,
icon,
to,
size: findVmTransfer(vmTask)?.size,
}
})
)
const { items: filteredVms, filter } = useQueryBuilderFilter<BackupLogVmFilterableData>(
'backup-log-vms',
() => vms.value
)
const schema = useQueryBuilderSchema<BackupLogVmFilterableData>({
'': useStringSchema(t('any-property')),
name: useStringSchema(t('vm')),
})
const { pageRecords: paginatedVms, paginationBindings } = usePagination('backup-log-vms', filteredVms)
const state = useTableState({
empty: () =>
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'],
body: (vm: BackupLogVmFilterableData) => {
const size = computed(() => formatSizeRaw(vm.size, 2))
return {
vm: r => r({ label: vm.name, to: vm.to, icon: vm.icon }),
diskSpace: r => r(size.value?.value, size.value?.prefix),
}
},
})
</script>
<style scoped lang="postcss">
.backup-log-backed-up-vms-table {
display: flex;
flex-direction: column;
gap: 2.4rem;
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<UiCard>
<UiTitle>
{{ t('general-information') }}
</UiTitle>
<VtsTabularKeyValueList>
<VtsTabularKeyValueRow :label="t('uuid')">
<template #value>
<VtsCodeSnippet :content="backupLog.id" />
</template>
</VtsTabularKeyValueRow>
<VtsTabularKeyValueRow :label="t('start-date')" :value="startDate" />
<VtsTabularKeyValueRow :label="t('end-date')" :value="endDate" />
<VtsTabularKeyValueRow :label="t('duration')" :value="duration" />
<VtsTabularKeyValueRow :label="t('status')">
<template #value>
<VtsStatus :status="backupLog.status" />
</template>
</VtsTabularKeyValueRow>
<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">
{{ task.properties.name ?? task.id }}
</UiLink>
</template>
</VtsTabularKeyValueRow>
</VtsTabularKeyValueList>
</UiCard>
</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 { useXoTaskCollection } from '@/modules/task/remote-resources/use-xo-task-collection.ts'
import { getTaskRoute } from '@/modules/task/utils/xo-task.util.ts'
import VtsCodeSnippet from '@core/components/code-snippet/VtsCodeSnippet.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import VtsTabularKeyValueList from '@core/components/tabular-key-value-list/VtsTabularKeyValueList.vue'
import VtsTabularKeyValueRow from '@core/components/tabular-key-value-row/VtsTabularKeyValueRow.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { formatSize } from '@core/utils/size.util.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { backupLog } = defineProps<{ backupLog: FrontXoBackupLog }>()
const { t } = useI18n()
const { getBackupLogDate, getBackupLogDuration, findTransferTaskSize } = useXoBackupLogsUtils()
const { tasks } = useXoTaskCollection()
const startDate = computed(() => getBackupLogDate(backupLog.start))
const endDate = computed(() => getBackupLogDate(backupLog.end))
const duration = computed(() => getBackupLogDuration(backupLog))
const transferSize = computed(() => {
const size = findTransferTaskSize(backupLog.tasks)
return size !== undefined ? formatSize(size, 2) : undefined
})
const task = computed(() => tasks.value.find(task => task.properties.runJobId === backupLog.id))
</script>

View File

@@ -0,0 +1,52 @@
<template>
<UiCard>
<UiTitle>
{{ t('schedule') }}
</UiTitle>
<VtsTabularKeyValueList>
<VtsTabularKeyValueRow :label="t('name')">
<template v-if="schedule !== undefined" #value>
<UiLink size="medium" icon="object:backup-schedule" :href="jobOverviewHref">
{{ schedule.name || t('untitled') }}
</UiLink>
</template>
</VtsTabularKeyValueRow>
<VtsTabularKeyValueRow :label="t('id')">
<template v-if="scheduleId !== undefined" #value>
<VtsCodeSnippet :content="scheduleId" />
</template>
</VtsTabularKeyValueRow>
</VtsTabularKeyValueList>
</UiCard>
</template>
<script setup lang="ts">
import type { FrontXoBackupLog } from '@/modules/backup-log/remote-resources/use-xo-backup-log-collection.ts'
import {
type FrontXoSchedule,
useXoScheduleCollection,
} from '@/modules/schedule/remote-resources/use-xo-schedule-collection.ts'
import { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
import VtsCodeSnippet from '@core/components/code-snippet/VtsCodeSnippet.vue'
import VtsTabularKeyValueList from '@core/components/tabular-key-value-list/VtsTabularKeyValueList.vue'
import VtsTabularKeyValueRow from '@core/components/tabular-key-value-row/VtsTabularKeyValueRow.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { backupLog } = defineProps<{ backupLog: FrontXoBackupLog }>()
const { t } = useI18n()
const { buildXo5Route } = useXoRoutes()
const { useGetScheduleById } = useXoScheduleCollection()
const scheduleId = computed(() => backupLog.scheduleId as FrontXoSchedule['id'] | undefined)
const schedule = useGetScheduleById(scheduleId)
const jobOverviewHref = computed(() => buildXo5Route('/backup/overview'))
</script>

View File

@@ -0,0 +1,53 @@
<template>
<div class="backup-log-header-breadcrumb">
<UiBreadcrumb :size>
<UiLink :size :to="{ name: '/admin/backup-and-replication/logs' }" icon="object:backup-archive">
{{ t('backup-and-replication') }}
</UiLink>
<UiLink :size :to="{ name: '/admin/backup-and-replication/logs' }">
{{ t('logs') }}
</UiLink>
<span class="log-name">
<VtsIcon name="object:backup-run" size="current" />
{{ logName }}
</span>
</UiBreadcrumb>
</div>
</template>
<script setup lang="ts">
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import UiBreadcrumb from '@core/components/ui/breadcrumb/UiBreadcrumb.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import { useUiStore } from '@core/stores/ui.store.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
defineProps<{ logName: string }>()
const { t } = useI18n()
const uiStore = useUiStore()
const size = computed(() => (uiStore.isSmall ? 'small' : 'medium'))
</script>
<style lang="postcss" scoped>
.backup-log-header-breadcrumb {
min-height: 5.6rem;
padding: 1.2rem 1.6rem;
display: flex;
gap: 1.6rem;
align-items: center;
border-block-end: 0.1rem solid var(--color-neutral-border);
background-color: var(--color-neutral-background-primary);
justify-content: space-between;
overflow-y: auto;
.log-name {
display: flex;
align-items: center;
gap: 1rem;
}
}
</style>

View File

@@ -111,11 +111,11 @@ describe('title', () => {
expect(title.text()).toContain('backup-log-42')
})
it('links to the backup repositories settings of XO 5', () => {
const wrapper = mountCard()
it('links to the detail page of the log', () => {
const wrapper = mountCard(createBackupLog({ id: 'backup-log-42' as FrontXoBackupLog['id'] }))
expect(wrapper.findComponent(UiPanelCardTitle).findComponent(UiLink).props('href')).toBe(
'https://xo5.example.com/#/settings/remotes'
expect(wrapper.findComponent(UiPanelCardTitle).findComponent(UiLink).attributes('href')).toBe(
'/admin/backup-log/backup-log-42/general'
)
})

View File

@@ -1,6 +1,6 @@
<template>
<UiPanelCard>
<UiPanelCardTitle :id="backupLog.id" size="medium" :label="logName" :href="logHref" icon="object:backup-run" />
<UiPanelCardTitle :id="backupLog.id" size="medium" :label="logName" :to="logTo" icon="object:backup-run" />
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('backup-date') }}</template>
@@ -94,7 +94,11 @@
import { useXoBackupJobCollection } from '@/modules/backup-job/remote-resources/use-xo-backup-job-collection.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 { getBackupLogSrIds, getBackupLogVmIds } from '@/modules/backup-log/utils/xo-backup-log.util.ts'
import {
getBackupLogRoute,
getBackupLogSrIds,
getBackupLogVmIds,
} from '@/modules/backup-log/utils/xo-backup-log.util.ts'
import { useXoSrUtils } from '@/modules/storage-repository/composables/xo-sr-utils.composable.ts'
import { useXoSrCollection } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import { useXoVmOperationLabel } from '@/modules/vm/composables/xo-vm-operation-label.composable.ts'
@@ -142,7 +146,7 @@ const job = useGetBackupJobById(() => backupLog.jobId)
const jobHref = computed(() => buildXo5Route('/settings/remotes'))
const logHref = computed(() => buildXo5Route('/settings/remotes'))
const logTo = computed(() => getBackupLogRoute(backupLog.id))
const vms = computed(() =>
getBackupLogVmIds(backupLog).map(id => {

View File

@@ -0,0 +1,84 @@
<template>
<div class="backup-log-run-tasks-list">
<UiTitle>
{{ t('run') }}
</UiTitle>
<UiQuerySearchBar @search="value => (searchQuery = value)" />
<div class="container">
<div class="actions">
<UiTableControlsBar :pagination-bindings />
</div>
<VtsStateHero v-if="convertedTasks.length === 0" format="table" type="no-data" size="small">
{{ t('no-task-detected') }}
</VtsStateHero>
<UiTaskList
v-else
:tasks="tasksRecords"
:selected-task-id="selectedTaskId"
@select="id => (selectedTaskId = id)"
/>
<VtsStateHero v-if="searchQuery && filteredTasks.length === 0" format="table" type="no-result" size="small">
{{ t('no-result') }}
</VtsStateHero>
<UiTableControlsBar :pagination-bindings />
</div>
</div>
</template>
<script setup lang="ts">
import { useXoBackupLogTasksConversion } from '@/modules/backup-log/composables/use-xo-backup-log-tasks-conversion.composable.ts'
import type { FrontXoBackupLog } from '@/modules/backup-log/remote-resources/use-xo-backup-log-collection.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiQuerySearchBar from '@core/components/ui/query-search-bar/UiQuerySearchBar.vue'
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 { usePagination } from '@core/composables/pagination.composable.ts'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { backupLog } = defineProps<{
backupLog: FrontXoBackupLog
}>()
const { t } = useI18n()
const { convertedTasks } = useXoBackupLogTasksConversion(() => backupLog)
const selectedTaskId = useRouteQuery('id')
const searchQuery = ref('')
const filteredTasks = computed(() => {
const searchTerm = searchQuery.value.trim().toLocaleLowerCase()
if (!searchTerm) {
return convertedTasks.value
}
return convertedTasks.value.filter(task =>
Object.values(task).some(value => String(value).toLocaleLowerCase().includes(searchTerm))
)
})
const { pageRecords: tasksRecords, paginationBindings } = usePagination('backup-log-tasks', filteredTasks)
</script>
<style scoped lang="postcss">
.backup-log-run-tasks-list,
.actions,
.container {
display: flex;
flex-direction: column;
}
.backup-log-run-tasks-list {
gap: 2.4rem;
.container,
.actions {
gap: 0.8rem;
}
}
</style>

View File

@@ -0,0 +1,32 @@
<template>
<VtsSidePanel :has-selection="!!task" @close="emit('close')">
<template v-if="task">
<BackupLogTaskQuickInfosCard :task />
<TaskInfosCard v-if="task.infos" :task />
<TaskWarningsCard v-if="task.warnings" :task />
<BackupLogTaskJsonCard :task />
<TaskErrorsCard v-if="isError" :task />
</template>
</VtsSidePanel>
</template>
<script setup lang="ts">
import BackupLogTaskJsonCard from '@/modules/backup-log/components/run/panel/cards/BackupLogTaskJsonCard.vue'
import BackupLogTaskQuickInfosCard from '@/modules/backup-log/components/run/panel/cards/BackupLogTaskQuickInfosCard.vue'
import type { BackupLogTask } from '@/modules/backup-log/composables/use-xo-backup-log-utils.composable.ts'
import TaskErrorsCard from '@/modules/task/components/list/panel/cards/TaskErrorsCard.vue'
import TaskInfosCard from '@/modules/task/components/list/panel/cards/TaskInfosCard.vue'
import TaskWarningsCard from '@/modules/task/components/list/panel/cards/TaskWarningsCard.vue'
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
import { computed } from 'vue'
const { task } = defineProps<{
task?: BackupLogTask
}>()
const emit = defineEmits<{
close: []
}>()
const isError = computed(() => task && task.result && (task.status === 'failure' || task.status === 'interrupted'))
</script>

View File

@@ -0,0 +1,20 @@
<template>
<UiPanelCard class="backup-log-task-json-card">
<UiPanelCardTitle size="medium" :label="t('task')" />
<UiLogEntryViewer :content="task" :label="t('json')" size="small" accent="info" />
</UiPanelCard>
</template>
<script lang="ts" setup>
import type { BackupLogTask } from '@/modules/backup-log/composables/use-xo-backup-log-utils.composable.ts'
import UiLogEntryViewer from '@core/components/ui/log-entry-viewer/UiLogEntryViewer.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
import { useI18n } from 'vue-i18n'
defineProps<{
task: BackupLogTask
}>()
const { t } = useI18n()
</script>

View File

@@ -0,0 +1,158 @@
<template>
<UiPanelCard class="backup-log-task-quick-infos-card">
<UiPanelCardTitle
:id="task.id"
size="medium"
:label="taskName.name"
:to="taskName.to"
:icon="taskName.icon ?? 'fa:bars-progress'"
/>
<div class="content">
<VtsCardRowKeyValue v-if="type !== undefined">
<template #key>{{ t('type') }}</template>
<template #value>{{ type }}</template>
<template #addons>
<VtsCopyButton :value="type" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue>
<template #key>{{ t('duration') }}</template>
<template #value>{{ duration }}</template>
<template v-if="duration !== undefined" #addons>
<VtsCopyButton :value="duration" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue v-if="speed !== undefined">
<template #key>{{ t('speed') }}</template>
<template #value>{{ speed }}</template>
<template #addons>
<VtsCopyButton :value="speed" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue v-if="size !== undefined">
<template #key>{{ t('size') }}</template>
<template #value>{{ size }}</template>
<template #addons>
<VtsCopyButton :value="size" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue>
<template #key>{{ t('task:started') }}</template>
<template #value>{{ startDate }}</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue>
<template #key>{{ t('task:ended') }}</template>
<template #value>{{ endDate ?? '-' }}</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue>
<template #key>{{ t('status') }}</template>
<template #value>
<UiTagsList>
<UiTag :accent="taskAccent" variant="secondary">
{{ task.status }}
</UiTag>
</UiTagsList>
</template>
<template #addons>
<UiCircleProgressBar :accent="progressAccent" size="small" :value="progress" />
</template>
</VtsCardRowKeyValue>
</div>
</UiPanelCard>
</template>
<script lang="ts" setup>
import { useXoBackupLogTaskName } from '@/modules/backup-log/composables/use-xo-backup-log-task-name.composable.ts'
import {
type BackupLogTask,
useXoBackupLogsUtils,
} from '@/modules/backup-log/composables/use-xo-backup-log-utils.composable.ts'
import { computeTransferSpeed } from '@/modules/backup-log/utils/xo-backup-log-transfer-speed.util.ts'
import { getTaskAccents } from '@/modules/task/utils/xo-task.util.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import UiCircleProgressBar from '@core/components/ui/circle-progress-bar/UiCircleProgressBar.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
import UiTag from '@core/components/ui/tag/UiTag.vue'
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
import { formatSize } from '@core/utils/size.util.ts'
import { formatSpeed } from '@core/utils/speed.util.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { task } = defineProps<{
task: BackupLogTask
}>()
const { t } = useI18n()
const { getTaskName } = useXoBackupLogTaskName()
const { getBackupLogDate, getBackupLogDuration, findVmTransfer } = useXoBackupLogsUtils()
const taskName = computed(() => getTaskName(task))
const type = computed(() => {
const exportTask = task.data?.type === 'VM' ? task.tasks?.find(subTask => subTask.data?.isFull !== undefined) : task
const isFull: unknown = exportTask?.data?.isFull
if (isFull === undefined) {
return undefined
}
return isFull ? t('full') : t('incremental')
})
const duration = computed(() => getBackupLogDuration(task))
const transfer = computed(() => {
if (task.data?.type === 'VM') {
return findVmTransfer(task)
}
if (task.message === 'transfer' && typeof task.result?.size === 'number') {
return { size: task.result.size, start: task.start, end: task.end }
}
return undefined
})
const size = computed(() => (transfer.value !== undefined ? formatSize(transfer.value.size, 2) : undefined))
const speed = computed(() => {
if (transfer.value === undefined) {
return undefined
}
const bytesPerSecond = computeTransferSpeed(transfer.value.size, transfer.value.start, transfer.value.end)
return bytesPerSecond !== undefined ? formatSpeed(bytesPerSecond) : undefined
})
const startDate = computed(() => getBackupLogDate(task.start))
const endDate = computed(() => getBackupLogDate(task.end))
const taskAccent = computed(() => getTaskAccents(task.status).statusAccent)
const progressAccent = computed(() => getTaskAccents(task.status).progressAccent)
const progress = computed(() => {
if (task.status === 'pending' && !task.end) {
return task.progress ?? 0
}
return task.progress ?? 100
})
</script>
<style scoped lang="postcss">
.backup-log-task-quick-infos-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -0,0 +1,77 @@
import type { BackupLogTask } from '@/modules/backup-log/composables/use-xo-backup-log-utils.composable.ts'
import {
type FrontXoBackupRepository,
useXoBackupRepositoryCollection,
} from '@/modules/backup-repository/remote-resources/use-xo-backup-repository-collection.ts'
import { getBackupRepositoryIcon } from '@/modules/backup-repository/utils/xo-backup-repository.util.ts'
import { useXoSrUtils } from '@/modules/storage-repository/composables/xo-sr-utils.composable.ts'
import {
type FrontXoSr,
useXoSrCollection,
} from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import { type FrontXoVm, useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import type { Task } from '@core/components/ui/task-item/UiTaskItem.vue'
import { objectIcon } from '@core/icons'
import { toLower } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { parse as parseBackupRepositoryUrl } from 'xo-remote-parser'
export function useXoBackupLogTaskName() {
const { t } = useI18n()
const { getVmById } = useXoVmCollection()
const { getSrById } = useXoSrCollection()
const { getBackupRepositoryById } = useXoBackupRepositoryCollection()
const { getSrStatusIcon } = useXoSrUtils()
function getTaskName(task: BackupLogTask): Pick<Task, 'name' | 'icon' | 'to'> {
const id = task.data?.id
switch (task.data?.type) {
case 'VM': {
const vm = getVmById(id as FrontXoVm['id'])
return vm !== undefined
? {
name: vm.name_label,
icon: objectIcon('vm', toLower(vm.power_state)),
to: { name: '/vm/[id]/dashboard', params: { id: vm.id } },
}
: { name: task.data.name_label ?? id }
}
case 'SR': {
const sr = getSrById(id as FrontXoSr['id'])
return sr !== undefined
? { name: sr.name_label, icon: getSrStatusIcon(sr), to: { name: '/sr/[id]/general', params: { id: sr.id } } }
: { name: id }
}
case 'remote': {
const br = getBackupRepositoryById(id as FrontXoBackupRepository['id'])
return br !== undefined
? {
name: br.name,
icon: getBackupRepositoryIcon(br, parseBackupRepositoryUrl(br.url).type),
to: { name: '/admin/backup-repository/[id]/general', params: { id: br.id } },
}
: { name: id }
}
}
switch (task.message) {
case 'snapshot':
return { name: t('snapshot') }
case 'transfer':
return { name: t('transfer') }
case 'merge':
return { name: t('merge') }
case 'health check':
return { name: t('health-check') }
default:
return { name: task.message }
}
}
return { getTaskName }
}

View File

@@ -0,0 +1,29 @@
import { useXoBackupLogTaskName } from '@/modules/backup-log/composables/use-xo-backup-log-task-name.composable.ts'
import type { BackupLogTask } 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 { Task } from '@core/components/ui/task-item/UiTaskItem.vue'
import { toComputed } from '@core/utils/to-computed.util.ts'
import { computed, type MaybeRefOrGetter } from 'vue'
export function useXoBackupLogTasksConversion(rawBackupLog: MaybeRefOrGetter<FrontXoBackupLog>) {
const backupLog = toComputed(rawBackupLog)
const { getTaskName } = useXoBackupLogTaskName()
function convertTask(task: BackupLogTask): Task {
return {
id: task.id,
status: task.status === 'interrupted' ? 'failure' : task.status,
...getTaskName(task),
start: task.start,
end: task.end,
infos: task.infos,
warnings: task.warnings,
subtasks: task.tasks?.map(convertTask),
}
}
const convertedTasks = computed(() => (backupLog.value.tasks ?? []).map(convertTask))
return { convertedTasks }
}

View File

@@ -1,4 +1,5 @@
import type { FrontXoBackupLog } from '@/modules/backup-log/remote-resources/use-xo-backup-log-collection.ts'
import { computeTransferSpeed } from '@/modules/backup-log/utils/xo-backup-log-transfer-speed.util.ts'
import { formatSizeRaw } from '@core/utils/size.util.ts'
import { formatDateTime } from '@core/utils/time.util.ts'
import type { Info, Scale } from 'human-format'
@@ -18,7 +19,7 @@ export function useXoBackupLogsUtils() {
return formatDateTime(value)
}
function getBackupLogDuration(backupLog: FrontXoBackupLog) {
function getBackupLogDuration(backupLog: Pick<FrontXoBackupLog, 'start' | 'end'>) {
if (backupLog.end === undefined || backupLog.start === undefined) {
return undefined
}
@@ -97,16 +98,13 @@ export function useXoBackupLogsUtils() {
const start = Math.min(...transfers.map(transfer => transfer.start))
const end = Math.max(...transfers.map(transfer => transfer.end))
if (end <= start) {
return undefined
}
return (size * 1000) / (end - start)
return computeTransferSpeed(size, start, end)
}
return {
getBackupLogDate,
getBackupLogDuration,
findVmTransfer,
getTransferSize,
findTransferTaskSize,
getTransferSpeed,

View File

@@ -1,5 +1,13 @@
import type { FrontXoBackupLog } from '@/modules/backup-log/remote-resources/use-xo-backup-log-collection.ts'
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import type { Task } from '@core/components/ui/task-item/UiTaskItem.vue'
export type BackupLogFilterableData = FrontXoBackupLog & {
jobName: string | undefined
}
export type BackupLogVmFilterableData = Pick<Task, 'icon' | 'to'> & {
id: FrontXoVm['id']
name: string
size: number | undefined
}

View File

@@ -0,0 +1,7 @@
export function computeTransferSpeed(size: number, start: number, end: number | undefined): number | undefined {
if (end === undefined || end <= start) {
return undefined
}
return (size * 1000) / (end - start)
}

View File

@@ -2,8 +2,9 @@ import type { FrontXoBackupLog } from '@/modules/backup-log/remote-resources/use
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import type { XoTask } from '@vates/types'
import type { RouteLocationAsRelative } from 'vue-router'
function getBackupLogVmTasks(backupLog: FrontXoBackupLog) {
export function getBackupLogVmTasks(backupLog: FrontXoBackupLog) {
return (backupLog.tasks ?? []).filter(task => task.data?.type === 'VM')
}
@@ -46,3 +47,10 @@ export function getBackupLogError(backupLog: FrontXoBackupLog): XoTask['result']
return (backupLog.result as XoTask['result']) ?? findTaskErrorRecursively(backupLog.tasks ?? [])
}
}
export function getBackupLogRoute(backupLogId: FrontXoBackupLog['id']): RouteLocationAsRelative {
return {
name: '/admin/backup-log/[id]/general',
params: { id: backupLogId },
}
}

View File

@@ -31,7 +31,7 @@ import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTi
import { useI18n } from 'vue-i18n'
defineProps<{
task: FrontXoTask
task: Pick<FrontXoTask, 'result'>
}>()
const { t } = useI18n()

View File

@@ -27,7 +27,7 @@ import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTi
import { useI18n } from 'vue-i18n'
defineProps<{
task: FrontXoTask
task: Pick<FrontXoTask, 'infos'>
}>()
const { t } = useI18n()

View File

@@ -31,7 +31,7 @@ import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTi
import { useI18n } from 'vue-i18n'
defineProps<{
task: FrontXoTask
task: Pick<FrontXoTask, 'warnings'>
}>()
const { t } = useI18n()

View File

@@ -0,0 +1,35 @@
<template>
<VtsStateHero v-if="!areBackupLogsReady" format="page" type="busy" size="large" />
<VtsStateHero v-else-if="!backupLog" format="page" type="not-found" size="large">
{{ t('object-not-found', { id: route.params.id }) }}
</VtsStateHero>
<RouterView v-else v-slot="{ Component }">
<BackupLogHeader v-if="uiStore.hasUi" :backup-log />
<component :is="Component" :backup-log />
</RouterView>
</template>
<script lang="ts" setup>
import BackupLogHeader from '@/modules/backup-log/components/BackupLogHeader.vue'
import {
type FrontXoBackupLog,
useXoBackupLogCollection,
} from '@/modules/backup-log/remote-resources/use-xo-backup-log-collection.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import { useDefaultTab } from '@core/composables/default-tab.composable.ts'
import { useUiStore } from '@core/stores/ui.store.ts'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
useDefaultTab('/admin/backup-log/[id]', 'general')
const route = useRoute<'/admin/backup-log/[id]'>()
const uiStore = useUiStore()
const { t } = useI18n()
const { areBackupLogsReady, useGetBackupLogById } = useXoBackupLogCollection()
const backupLog = useGetBackupLogById(() => route.params.id as FrontXoBackupLog['id'])
</script>

View File

@@ -0,0 +1,24 @@
<template>
<div class="backed-up-vms">
<UiCard class="container">
<BackupLogBackedUpVmsTable :backup-log />
</UiCard>
</div>
</template>
<script setup lang="ts">
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 UiCard from '@core/components/ui/card/UiCard.vue'
defineProps<{ backupLog: FrontXoBackupLog }>()
</script>
<style scoped lang="postcss">
.backed-up-vms {
.container {
height: fit-content;
margin: 0.8rem;
}
}
</style>

View File

@@ -0,0 +1,20 @@
<template>
<VtsColumns extra-space-around>
<VtsColumn>
<BackupLogGeneralInfoCard :backup-log />
</VtsColumn>
<VtsColumn>
<BackupLogScheduleCard :backup-log />
</VtsColumn>
</VtsColumns>
</template>
<script setup lang="ts">
import BackupLogGeneralInfoCard from '@/modules/backup-log/components/cards/BackupLogGeneralInfoCard.vue'
import BackupLogScheduleCard from '@/modules/backup-log/components/cards/BackupLogScheduleCard.vue'
import type { FrontXoBackupLog } from '@/modules/backup-log/remote-resources/use-xo-backup-log-collection.ts'
import VtsColumn from '@core/components/column/VtsColumn.vue'
import VtsColumns from '@core/components/columns/VtsColumns.vue'
defineProps<{ backupLog: FrontXoBackupLog }>()
</script>

View File

@@ -0,0 +1,37 @@
<template>
<VtsContentSidePanel class="run">
<UiCard class="container">
<BackupLogRunTasksList :backup-log />
</UiCard>
<BackupLogTaskSidePanel :task="selectedTask" @close="selectedTask = undefined" />
</VtsContentSidePanel>
</template>
<script setup lang="ts">
import BackupLogRunTasksList from '@/modules/backup-log/components/run/BackupLogRunTasksList.vue'
import BackupLogTaskSidePanel from '@/modules/backup-log/components/run/panel/BackupLogTaskSidePanel.vue'
import type { BackupLogTask } 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 { FrontXoTask } from '@/modules/task/remote-resources/use-xo-task-collection.ts'
import { findTaskById } from '@/modules/task/utils/xo-task.util.ts'
import VtsContentSidePanel from '@core/components/layout/VtsContentSidePanel.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
const { backupLog } = defineProps<{ backupLog: FrontXoBackupLog }>()
const selectedTask = useRouteQuery<BackupLogTask | undefined>('id', {
toData: id => findTaskById(backupLog.tasks ?? [], id as FrontXoTask['id']) as BackupLogTask | undefined,
toQuery: task => task?.id ?? '',
})
</script>
<style scoped lang="postcss">
.run {
.container {
height: fit-content;
gap: 4rem;
margin: 0.8rem;
}
}
</style>

View File

@@ -113,6 +113,36 @@ declare module 'vue-router/auto-routes' {
Record<never, never>,
| never
>,
'/admin/backup-log/[id]': RouteRecordInfo<
'/admin/backup-log/[id]',
'/admin/backup-log/:id',
{ id: ParamValue<true> },
{ id: ParamValue<false> },
| '/admin/backup-log/[id]/backed-up-vms'
| '/admin/backup-log/[id]/general'
| '/admin/backup-log/[id]/run'
>,
'/admin/backup-log/[id]/backed-up-vms': RouteRecordInfo<
'/admin/backup-log/[id]/backed-up-vms',
'/admin/backup-log/:id/backed-up-vms',
{ id: ParamValue<true> },
{ id: ParamValue<false> },
| never
>,
'/admin/backup-log/[id]/general': RouteRecordInfo<
'/admin/backup-log/[id]/general',
'/admin/backup-log/:id/general',
{ id: ParamValue<true> },
{ id: ParamValue<false> },
| never
>,
'/admin/backup-log/[id]/run': RouteRecordInfo<
'/admin/backup-log/[id]/run',
'/admin/backup-log/:id/run',
{ id: ParamValue<true> },
{ id: ParamValue<false> },
| never
>,
'/admin/backup-repository/[id]': RouteRecordInfo<
'/admin/backup-repository/[id]',
'/admin/backup-repository/:id',
@@ -636,6 +666,33 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
'src/pages/admin/backup-log/[id].vue': {
routes:
| '/admin/backup-log/[id]'
| '/admin/backup-log/[id]/backed-up-vms'
| '/admin/backup-log/[id]/general'
| '/admin/backup-log/[id]/run'
views:
| 'default'
}
'src/pages/admin/backup-log/[id]/backed-up-vms.vue': {
routes:
| '/admin/backup-log/[id]/backed-up-vms'
views:
| never
}
'src/pages/admin/backup-log/[id]/general.vue': {
routes:
| '/admin/backup-log/[id]/general'
views:
| never
}
'src/pages/admin/backup-log/[id]/run.vue': {
routes:
| '/admin/backup-log/[id]/run'
views:
| never
}
'src/pages/admin/backup-repository/[id].vue': {
routes:
| '/admin/backup-repository/[id]'

View File

@@ -37,8 +37,7 @@
> - major: if the change breaks compatibility
>
> Keep this list alphabetically ordered to avoid merge conflicts
<!--packages-start-->
> yarn<!--packages-start-->
- @vates/types minor
- @xen-orchestra/backups minor