mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-10-12 02:24:05 +00:00
feat(xo6): implement vm snapshot tab page (#9521)
This commit is contained in:
@@ -93,7 +93,7 @@ type BaseXoVm = BaseXapiXo & {
|
||||
notes?: string
|
||||
os_version: Record<string, string> | null
|
||||
other: Record<string, string>
|
||||
parent?: XoVm['id']
|
||||
parent?: XoVmSnapshot['id']
|
||||
power_state: VM_POWER_STATE
|
||||
pvDriversDetected?: boolean
|
||||
pvDriversUpToDate?: boolean
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<template>
|
||||
<UiTableCell>
|
||||
<UiLink size="medium" :icon :to :href :target class="link">
|
||||
<slot />
|
||||
<VtsIcon v-if="rightIcon" v-tooltip="rightIcon.tooltip ?? false" :name="rightIcon.icon" size="medium" />
|
||||
</UiLink>
|
||||
<div class="vts-link-cell">
|
||||
<UiLink size="medium" :icon :to :href :target class="link">
|
||||
<slot />
|
||||
<VtsIcon v-if="rightIcon" v-tooltip="rightIcon.tooltip ?? false" :name="rightIcon.icon" size="medium" />
|
||||
</UiLink>
|
||||
<span v-if="slots.suffix" class="interpunct" />
|
||||
<slot name="suffix" />
|
||||
</div>
|
||||
</UiTableCell>
|
||||
</template>
|
||||
|
||||
@@ -24,9 +28,24 @@ export type VtsLinkCellProps = LinkOptions & {
|
||||
}
|
||||
|
||||
defineProps<VtsLinkCellProps>()
|
||||
|
||||
const slots = defineSlots<{
|
||||
default(): any
|
||||
suffix?(): any
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.vts-link-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.interpunct::before {
|
||||
content: '•';
|
||||
}
|
||||
|
||||
.link {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
faArrowLeft,
|
||||
faBarsProgress,
|
||||
faBoxesStacked,
|
||||
faCamera,
|
||||
faCircle,
|
||||
faCity,
|
||||
faClock,
|
||||
@@ -118,6 +119,21 @@ const schedule = defineIcon([
|
||||
},
|
||||
])
|
||||
|
||||
const camera = defineIcon([
|
||||
{
|
||||
icon: faCircle,
|
||||
color: 'var(--color-neutral-background-primary)',
|
||||
translate: [7, 5.5],
|
||||
size: 13,
|
||||
},
|
||||
{
|
||||
icon: faCamera,
|
||||
color: 'var(--color-neutral-txt-primary)',
|
||||
translate: [7, 5.5],
|
||||
size: 10,
|
||||
},
|
||||
])
|
||||
|
||||
export const objectIcons = defineIconPack({
|
||||
instance: constructIcon(faSatellite),
|
||||
pool: constructIcon(faCity),
|
||||
@@ -159,6 +175,12 @@ export const objectIcons = defineIconPack({
|
||||
'vm:suspended': [constructIcon(faDesktop), ...constructCircleStatus('suspended-circle')],
|
||||
'vm:warning': [constructIcon(faDesktop), ...constructCircleStatus('warning-circle')],
|
||||
'vm:halted': [constructIcon(faDesktop), ...constructCircleStatus('halted-circle')],
|
||||
'vm-snapshot': [
|
||||
constructIcon(faDesktop),
|
||||
{
|
||||
icon: camera,
|
||||
},
|
||||
],
|
||||
sr: constructIcon(faDatabase),
|
||||
'sr:unknown': [
|
||||
{
|
||||
|
||||
@@ -238,6 +238,7 @@
|
||||
"crash-dump-storage-repository": "Crash dump storage repository",
|
||||
"created-by": "Created by",
|
||||
"created-on": "Created on",
|
||||
"creation-date": "Creation date",
|
||||
"cron-pattern": "Cron pattern",
|
||||
"current": "Current",
|
||||
"current-attach": "Current attach",
|
||||
@@ -333,6 +334,7 @@
|
||||
"hosts-status": "Hosts status",
|
||||
"hyper-threading": "Hyper threading (SMT)",
|
||||
"id": "ID",
|
||||
"included": "Included",
|
||||
"in-last-three-runs": "In their last 3 runs",
|
||||
"in-progress": "In progress",
|
||||
"info": "Info",
|
||||
@@ -428,7 +430,9 @@
|
||||
"last-n-backup-archives": "Last backup archive | Last {n} backup archives",
|
||||
"last-n-backup-runs": "Last backup run | Last {n} backup runs",
|
||||
"last-n-runs": "Last run | Last {n} runs",
|
||||
"last-revert": "Last revert",
|
||||
"last-run-number": "Last run #{n}",
|
||||
"last-snapshot": "Last snapshot",
|
||||
"last-week": "Last week",
|
||||
"learn-more": "Learn more",
|
||||
"lets-go!": "Let's go!",
|
||||
@@ -453,11 +457,13 @@
|
||||
"management-agent-not-detected": "Management agent not detected",
|
||||
"management-agent-version": "Management agent version",
|
||||
"management-ip": "Management IP",
|
||||
"manual": "Manual",
|
||||
"manufacturer-info": "Manufacturer info",
|
||||
"master": "Primary host",
|
||||
"maximum-cpu-limit": "Maximum CPU limit",
|
||||
"maximum-dynamic-memory": "Maximum dynamic memory",
|
||||
"maximum-static-memory": "Maximum static memory",
|
||||
"memory": "Memory",
|
||||
"memory-usage": "Memory usage",
|
||||
"merge-backups-synchronously": "Merge backups synchronously",
|
||||
"message": "Message",
|
||||
@@ -553,6 +559,7 @@
|
||||
"no-selected-vm-can-be-exported": "No selected VM can be exported",
|
||||
"no-selected-vm-can-be-migrated": "No selected VM can be migrated",
|
||||
"no-server-detected": "No servers detected",
|
||||
"no-snapshot-detected": "No snapshots detected",
|
||||
"no-storage-repository-detected": "No storage repositories detected",
|
||||
"no-task": "No tasks",
|
||||
"no-task-detected": "No tasks detected",
|
||||
@@ -564,6 +571,7 @@
|
||||
"none": "None",
|
||||
"normal": "Normal",
|
||||
"not-found": "Not found",
|
||||
"not-included": "Not included",
|
||||
"not-resident-on": "Not resident on",
|
||||
"not-running": "Not running",
|
||||
"not-yet-available": "Information is not yet available",
|
||||
@@ -740,6 +748,7 @@
|
||||
"skipped": "Skipped",
|
||||
"smart-mode": "Smart mode",
|
||||
"snapshot": "Snapshot",
|
||||
"snapshot-created-on": "Snapshot created on",
|
||||
"snapshot-mode": "Snapshot mode",
|
||||
"snapshots": "Snapshots",
|
||||
"sockets-with-cores-per-socket": "{nSockets} sockets × {nCores} cores/socket",
|
||||
@@ -841,11 +850,13 @@
|
||||
"total-used:": "Total used:",
|
||||
"transfer-size": "Transfer size",
|
||||
"translation-tool": "Translation tool",
|
||||
"trigger": "Trigger",
|
||||
"unable-to-connect-to": "Unable to connect to {ip}",
|
||||
"unable-to-connect-to-the-pool": "Unable to connect to the pool",
|
||||
"unable-to-connect-to-xo-server": "Unable to connect to XO server.",
|
||||
"unknown": "Unknown",
|
||||
"unlocked": "Unlocked",
|
||||
"unnamed": "Unnamed",
|
||||
"unreachable-hosts": "Unreachable hosts",
|
||||
"unreachable-hosts-reload-page": "Done, reload the page",
|
||||
"untitled": "Untitled",
|
||||
@@ -854,6 +865,7 @@
|
||||
"used": "Used",
|
||||
"used-for-backup": "Used for backup",
|
||||
"used-space": "Used space",
|
||||
"used-space-on-sr": "Used space on SR",
|
||||
"user-config": "User config",
|
||||
"username": "Username",
|
||||
"uuid": "UUID",
|
||||
|
||||
@@ -238,6 +238,7 @@
|
||||
"crash-dump-storage-repository": "Dépôt de stockage des log de crash",
|
||||
"created-by": "Créé par",
|
||||
"created-on": "Créé le",
|
||||
"creation-date": "Date de création",
|
||||
"cron-pattern": "Pattern cron",
|
||||
"current": "Actuel",
|
||||
"current-attach": "Attachement actuel",
|
||||
@@ -333,6 +334,7 @@
|
||||
"hosts-status": "Statut des hôtes",
|
||||
"hyper-threading": "Hyper-threading (SMT)",
|
||||
"id": "ID",
|
||||
"included": "Inclus",
|
||||
"in-last-three-runs": "Dans leurs trois derniers runs",
|
||||
"in-progress": "En cours",
|
||||
"info": "Info",
|
||||
@@ -428,7 +430,9 @@
|
||||
"last-n-backup-archives": "Dernier run d'archivage | {n} derniers archivages",
|
||||
"last-n-backup-runs": "Dernier run de sauvegarde | {n} dernières sauvegardes",
|
||||
"last-n-runs": "Dernier run | {n} derniers runs",
|
||||
"last-revert": "Dernière restauration",
|
||||
"last-run-number": "Dernière exécution #{n}",
|
||||
"last-snapshot": "Dernier instantané",
|
||||
"last-week": "Semaine dernière",
|
||||
"learn-more": "En savoir plus",
|
||||
"lets-go!": "Allons-y !",
|
||||
@@ -453,11 +457,13 @@
|
||||
"management-agent-not-detected": "Agent de gestion non détecté",
|
||||
"management-agent-version": "Version de l'agent de gestion",
|
||||
"management-ip": "IP principale",
|
||||
"manual": "Manuel",
|
||||
"manufacturer-info": "Informations sur le fabricant",
|
||||
"master": "Hôte primaire",
|
||||
"maximum-cpu-limit": "Nombre maximal de CPU",
|
||||
"maximum-dynamic-memory": "Mémoire dynamique maximale",
|
||||
"maximum-static-memory": "Mémoire statique maximale",
|
||||
"memory": "Mémoire",
|
||||
"memory-usage": "Utilisation de la mémoire",
|
||||
"merge-backups-synchronously": "Fusionner les sauvegardes de manière synchrone",
|
||||
"message": "Message",
|
||||
@@ -553,6 +559,7 @@
|
||||
"no-selected-vm-can-be-exported": "Aucune VM sélectionnée ne peut être exportée",
|
||||
"no-selected-vm-can-be-migrated": "Aucune VM sélectionnée ne peut être migrée",
|
||||
"no-server-detected": "Aucun serveur détecté",
|
||||
"no-snapshot-detected": "Aucun instantané détecté",
|
||||
"no-storage-repository-detected": "Aucun dépôt de stockage détecté",
|
||||
"no-task": "Aucune tâche",
|
||||
"no-task-detected": "Aucune tâche détectée",
|
||||
@@ -564,6 +571,7 @@
|
||||
"none": "Aucun",
|
||||
"normal": "Normal",
|
||||
"not-found": "Non trouvé",
|
||||
"not-included": "Non inclus",
|
||||
"not-resident-on": "Non résident sur",
|
||||
"not-running": "Pas en cours d'exécution",
|
||||
"not-yet-available": "Information pas encore disponible",
|
||||
@@ -740,6 +748,7 @@
|
||||
"skipped": "Ignoré",
|
||||
"smart-mode": "Mode intelligent",
|
||||
"snapshot": "Instantané",
|
||||
"snapshot-created-on": "Instantané créé le",
|
||||
"snapshot-mode": "Mode de sauvegarde",
|
||||
"snapshots": "Instantanés",
|
||||
"sockets-with-cores-per-socket": "{nSockets} sockets × {nCores} cœurs/socket",
|
||||
@@ -841,11 +850,13 @@
|
||||
"total-used:": "Total utilisé :",
|
||||
"transfer-size": "Taille transférée",
|
||||
"translation-tool": "Outil de traduction",
|
||||
"trigger": "Déclencheur",
|
||||
"unable-to-connect-to": "Impossible de se connecter à {ip}",
|
||||
"unable-to-connect-to-the-pool": "Impossible de se connecter au Pool",
|
||||
"unable-to-connect-to-xo-server": "Impossible de se connecter au serveur XO.",
|
||||
"unknown": "Inconnu",
|
||||
"unlocked": "Déverrouillé",
|
||||
"unnamed": "Sans nom",
|
||||
"unreachable-hosts": "Hôtes inaccessibles",
|
||||
"unreachable-hosts-reload-page": "C'est fait. Rafraîchir la page",
|
||||
"untitled": "Sans titre",
|
||||
@@ -854,6 +865,7 @@
|
||||
"used": "Utilisé",
|
||||
"used-for-backup": "Utilisé pour la sauvegarde",
|
||||
"used-space": "Espace utilisé",
|
||||
"used-space-on-sr": "Espace utilisé sur le SR",
|
||||
"user-config": "Configuration utilisateur",
|
||||
"username": "Nom d'utilisateur",
|
||||
"uuid": "UUID",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import VtsLinkCell, { type VtsLinkCellProps } from '@core/components/table/cells/VtsLinkCell.vue'
|
||||
import VtsTextCell from '@core/components/table/cells/VtsTextCell.vue'
|
||||
import { defineColumn } from '@core/packages/table'
|
||||
import { renderHeadCell } from '@core/tables/helpers/render-head-cell.ts'
|
||||
import type { HeaderConfig } from '@core/tables/types.ts'
|
||||
import { h } from 'vue'
|
||||
|
||||
export const useLinkOrTextColumn = defineColumn((config?: HeaderConfig) => ({
|
||||
renderHead: () => renderHeadCell(config?.headerIcon, config?.headerLabel),
|
||||
renderBody: (link: { label: string; suffix?: string } & VtsLinkCellProps) => {
|
||||
const { label, suffix, ...linkCellProps } = link
|
||||
|
||||
if (!linkCellProps.href && !linkCellProps.to) {
|
||||
return h(VtsTextCell, () => label)
|
||||
}
|
||||
|
||||
return h(VtsLinkCell, linkCellProps, {
|
||||
default: () => label,
|
||||
suffix: suffix ? () => h('span', suffix) : undefined,
|
||||
})
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineColumns } from '@core/packages/table'
|
||||
import { useDateColumn } from '@core/tables/column-definitions/date-column.ts'
|
||||
import { useLinkColumn } from '@core/tables/column-definitions/link-column.ts'
|
||||
import { useLinkOrTextColumn } from '@core/tables/column-definitions/link-or-text-column.ts'
|
||||
import { useSelectItemColumn } from '@core/tables/column-definitions/select-item-column.ts'
|
||||
import { useTruncatedTextColumn } from '@core/tables/column-definitions/truncated-text-column.ts'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useSnapshotColumns = defineColumns(() => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
name: useLinkColumn({ headerLabel: () => t('name') }),
|
||||
description: useTruncatedTextColumn({ headerLabel: () => t('description') }),
|
||||
creationDate: useDateColumn({ headerLabel: () => t('creation-date'), dateStyle: 'short', timeStyle: 'medium' }),
|
||||
trigger: useLinkOrTextColumn({ headerLabel: () => t('trigger'), headerIcon: 'fa:square-caret-down' }),
|
||||
selectItem: useSelectItemColumn(),
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="snapshots-table">
|
||||
<UiTitle>
|
||||
{{ t('snapshots') }}
|
||||
<template #action>
|
||||
<UiButton
|
||||
:busy="isRunning"
|
||||
variant="primary"
|
||||
accent="brand"
|
||||
size="medium"
|
||||
left-icon="fa:plus"
|
||||
@click="snapshotJob()"
|
||||
>
|
||||
{{ t('new') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiTitle>
|
||||
<div class="container">
|
||||
<div class="table-actions">
|
||||
<UiQuerySearchBar @search="(value: string) => (searchQuery = value)" />
|
||||
</div>
|
||||
|
||||
<VtsTable :state :pagination-bindings sticky="right">
|
||||
<thead>
|
||||
<tr>
|
||||
<HeadCells />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<VtsRow
|
||||
v-for="snapshot of paginatedSnapshots"
|
||||
:key="snapshot.id"
|
||||
:selected="selectedSnapshotId === snapshot.id"
|
||||
>
|
||||
<BodyCells :item="snapshot" />
|
||||
</VtsRow>
|
||||
</tbody>
|
||||
</VtsTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type FrontXoVmSnapshot,
|
||||
useXoVmSnapshotCollection,
|
||||
} from '@/modules/snapshot/components/remote-resources/use-xo-vm-snapshot-collection.ts'
|
||||
import { useSnapshotTrigger } from '@/modules/snapshot/composables/xo-snapshot-trigger.composable.ts'
|
||||
import { useXo5VmSnapshotRoute } from '@/modules/snapshot/composables/xo-vm-snapshot-route-xo5.composable.ts'
|
||||
import { useXoVmSnapshotJob } from '@/modules/vm/jobs/xo-vm-snapshot.job.ts'
|
||||
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
|
||||
import VtsRow from '@core/components/table/VtsRow.vue'
|
||||
import VtsTable from '@core/components/table/VtsTable.vue'
|
||||
import UiButton from '@core/components/ui/button/UiButton.vue'
|
||||
import UiQuerySearchBar from '@core/components/ui/query-search-bar/UiQuerySearchBar.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 { useTableState } from '@core/composables/table-state.composable.ts'
|
||||
import { useSnapshotColumns } from '@core/tables/column-sets/snapshot-columns.ts'
|
||||
import { useSorted } from '@vueuse/core'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { snapshots: rawSnapshots, vm } = defineProps<{
|
||||
snapshots: FrontXoVmSnapshot[]
|
||||
vm: FrontXoVm
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { areSnapshotsReady, hasSnapshotFetchError } = useXoVmSnapshotCollection()
|
||||
|
||||
const { getSnapshotTrigger } = useSnapshotTrigger()
|
||||
|
||||
const { buildXo5VmSnapshotRoute } = useXo5VmSnapshotRoute()
|
||||
|
||||
const { run: snapshotJob, isRunning } = useXoVmSnapshotJob(() => [vm])
|
||||
|
||||
const selectedSnapshotId = useRouteQuery('id')
|
||||
|
||||
const searchQuery = ref('')
|
||||
|
||||
const filteredSnapshots = computed(() => {
|
||||
const searchTerm = searchQuery.value.trim().toLocaleLowerCase()
|
||||
|
||||
if (!searchTerm) {
|
||||
return rawSnapshots
|
||||
}
|
||||
|
||||
return rawSnapshots.filter(snapshot =>
|
||||
Object.values(snapshot).some(value => String(value).toLocaleLowerCase().includes(searchTerm))
|
||||
)
|
||||
})
|
||||
|
||||
const sortedSnapshots = useSorted(
|
||||
filteredSnapshots,
|
||||
(snapshot1, snapshot2) => snapshot2.snapshot_time - snapshot1.snapshot_time
|
||||
)
|
||||
|
||||
const { pageRecords: paginatedSnapshots, paginationBindings } = usePagination('snapshots', sortedSnapshots)
|
||||
|
||||
const state = useTableState({
|
||||
busy: !areSnapshotsReady,
|
||||
error: hasSnapshotFetchError,
|
||||
empty: () =>
|
||||
rawSnapshots.length === 0
|
||||
? t('no-snapshot-detected')
|
||||
: filteredSnapshots.value.length === 0
|
||||
? { type: 'no-result' }
|
||||
: false,
|
||||
})
|
||||
|
||||
const { HeadCells, BodyCells } = useSnapshotColumns({
|
||||
body: (snapshot: FrontXoVmSnapshot) => {
|
||||
return {
|
||||
name: r =>
|
||||
r({
|
||||
label: snapshot.name_label,
|
||||
icon: 'object:vm-snapshot',
|
||||
href: buildXo5VmSnapshotRoute(vm.id, snapshot.id),
|
||||
}),
|
||||
description: r => r(snapshot.name_description),
|
||||
creationDate: r => r(snapshot.snapshot_time * 1000),
|
||||
trigger: r => r(getSnapshotTrigger(snapshot)),
|
||||
selectItem: r => r(() => (selectedSnapshotId.value = snapshot.id)),
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.snapshots-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
|
||||
.table-actions,
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<UiPanel :class="{ 'mobile-drawer': uiStore.isSmall }">
|
||||
<template #header>
|
||||
<div :class="{ 'action-buttons-container': uiStore.isSmall }">
|
||||
<UiButtonIcon
|
||||
v-tooltip="t('action:close')"
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
accent="brand"
|
||||
:icon="uiStore.isSmall ? 'fa:angle-left' : 'fa:close'"
|
||||
@click="emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<SnapshotInfoCard :snapshot />
|
||||
<SnapshotVdiCard :snapshot />
|
||||
</template>
|
||||
</UiPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SnapshotInfoCard from '@/modules/snapshot/components/list/panel/cards/SnapshotInfoCard.vue'
|
||||
import SnapshotVdiCard from '@/modules/snapshot/components/list/panel/cards/SnapshotVdiCard.vue'
|
||||
import type { FrontXoVmSnapshot } from '@/modules/snapshot/components/remote-resources/use-xo-vm-snapshot-collection.ts'
|
||||
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
||||
import UiPanel from '@core/components/ui/panel/UiPanel.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive.ts'
|
||||
import { useUiStore } from '@core/stores/ui.store.ts'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<{ snapshot: FrontXoVmSnapshot }>()
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const uiStore = useUiStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.mobile-drawer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
.action-buttons-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<UiLink :icon="vbdsSnapshotStatus" size="medium" :href="xo5VmSnapshotVdiHref">{{ vdi.name_label }}</UiLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FrontXoVmSnapshot } from '@/modules/snapshot/components/remote-resources/use-xo-vm-snapshot-collection.ts'
|
||||
import type { FrontXoVmSnapshotVdi } from '@/modules/snapshot/components/remote-resources/use-xo-vm-snapshot-vdi-collection.ts'
|
||||
import { useVbdsStatus, type VbdAttachmentStatus } from '@/modules/vbd/composables/use-vbds-status.composable.ts'
|
||||
import { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
|
||||
import type { IconName } from '@core/icons'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import { useMapper } from '@core/packages/mapper'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { snapshot, vdi } = defineProps<{ snapshot: FrontXoVmSnapshot; vdi: FrontXoVmSnapshotVdi }>()
|
||||
|
||||
const { buildXo5Route } = useXoRoutes()
|
||||
const xo5VmSnapshotVdiHref = computed(() => buildXo5Route(`/vms/${snapshot.id}/disks`))
|
||||
|
||||
const vbdsStatus = useVbdsStatus(() => vdi.$VBDs)
|
||||
|
||||
const vbdsSnapshotStatus = useMapper<VbdAttachmentStatus, IconName>(
|
||||
() => vbdsStatus.value,
|
||||
{
|
||||
allAttached: 'object:vdi:attached',
|
||||
someAttached: 'object:vdi:disabled',
|
||||
noneAttached: 'object:vdi:detached',
|
||||
},
|
||||
'someAttached'
|
||||
)
|
||||
</script>
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>
|
||||
{{ t('used-space-on-sr') }}
|
||||
</template>
|
||||
<template #value>{{ formattedSize }}</template>
|
||||
<template #addons>
|
||||
<VtsCopyButton :value="formattedSize" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
|
||||
import { formatSize } from '@core/utils/size.util.ts'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { usage } = defineProps<{ usage: number }>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const formattedSize = computed(() => formatSize(usage, 2))
|
||||
</script>
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<UiCard class="card-container">
|
||||
<div class="content">
|
||||
<UiLink icon="object:vm" size="medium" :href="buildXo5VmSnapshotRoute(snapshot.$snapshot_of, snapshot.id)">
|
||||
{{ snapshot.name_label }}
|
||||
</UiLink>
|
||||
<VtsCodeSnippet :content="snapshot.id" copy />
|
||||
</div>
|
||||
<div class="content">
|
||||
<!-- DESCRIPTION -->
|
||||
<VtsCardRowKeyValue truncate align-top>
|
||||
<template #key>
|
||||
{{ t('description') }}
|
||||
</template>
|
||||
<template #value>{{ snapshot.name_description }}</template>
|
||||
<template v-if="snapshot.name_description" #addons>
|
||||
<VtsCopyButton :value="snapshot.name_description" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<!-- CREATION DATE -->
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>
|
||||
{{ t('creation-date') }}
|
||||
</template>
|
||||
<template #value>
|
||||
{{ formattedDate }}
|
||||
</template>
|
||||
<template #addons>
|
||||
<VtsCopyButton :value="formattedDate" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<!-- TRIGGER -->
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>
|
||||
{{ t('trigger') }}
|
||||
</template>
|
||||
<template #value>
|
||||
<UiLink v-if="snapshot.other?.['xo:backup:schedule']" size="small" :icon="trigger.icon" :href="trigger.href">
|
||||
{{ trigger.label }}
|
||||
</UiLink>
|
||||
<span v-else>
|
||||
{{ trigger.label }}
|
||||
</span>
|
||||
</template>
|
||||
<template #addons>
|
||||
<VtsCopyButton :value="trigger.label" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<!-- MEMORY -->
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>
|
||||
{{ t('memory') }}
|
||||
</template>
|
||||
<template #value>
|
||||
<UiInfo :accent="memoryAccent" size="medium">
|
||||
{{ t(memoryLabel) }}
|
||||
</UiInfo>
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
</div>
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FrontXoVmSnapshot } from '@/modules/snapshot/components/remote-resources/use-xo-vm-snapshot-collection.ts'
|
||||
import { useSnapshotTrigger } from '@/modules/snapshot/composables/xo-snapshot-trigger.composable.ts'
|
||||
import { useXo5VmSnapshotRoute } from '@/modules/snapshot/composables/xo-vm-snapshot-route-xo5.composable.ts'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsCodeSnippet from '@core/components/code-snippet/VtsCodeSnippet.vue'
|
||||
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
import UiInfo from '@core/components/ui/info/UiInfo.vue'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import { VM_POWER_STATE } from '@vates/types'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { snapshot } = defineProps<{ snapshot: FrontXoVmSnapshot }>()
|
||||
|
||||
const { t, d } = useI18n()
|
||||
|
||||
const { getSnapshotTrigger } = useSnapshotTrigger()
|
||||
|
||||
const { buildXo5VmSnapshotRoute } = useXo5VmSnapshotRoute()
|
||||
|
||||
const formattedDate = computed(() => d(snapshot.snapshot_time * 1000, { dateStyle: 'short', timeStyle: 'medium' }))
|
||||
|
||||
const memoryAccent = computed(() => (snapshot.power_state === VM_POWER_STATE.SUSPENDED ? 'success' : 'muted'))
|
||||
|
||||
const memoryLabel = computed(() => (snapshot.power_state === VM_POWER_STATE.SUSPENDED ? 'included' : 'not-included'))
|
||||
|
||||
const trigger = computed(() => getSnapshotTrigger(snapshot))
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.card-container {
|
||||
gap: 1.6rem;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<UiCard v-if="vmSnapshotVdis.length > 0" class="card-container">
|
||||
<UiCardTitle>
|
||||
{{ t('vdis') }}
|
||||
<UiCounter :value="vmSnapshotVdis.length" accent="neutral" size="small" variant="primary" />
|
||||
</UiCardTitle>
|
||||
<div class="content">
|
||||
<div v-for="(vmSnapshotVdi, index) in vmSnapshotVdis" :key="vmSnapshotVdi.id" class="content">
|
||||
<SnapshotVdiLinkCardItem :vdi="vmSnapshotVdi" :snapshot />
|
||||
<!-- DESCRIPTION -->
|
||||
<VtsCardRowKeyValue truncate>
|
||||
<template #key>
|
||||
{{ t('description') }}
|
||||
</template>
|
||||
<template #value>{{ vmSnapshotVdi.name_description }}</template>
|
||||
<template v-if="vmSnapshotVdi.name_description" #addons>
|
||||
<VtsCopyButton :value="vmSnapshotVdi.name_description" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<!-- USED SPACE -->
|
||||
<SnapshotVdiUsageCardItem :usage="vmSnapshotVdi.usage" />
|
||||
<!-- FORMAT -->
|
||||
<VdiFormatCardItem :format="vmSnapshotVdi.image_format" />
|
||||
<VtsDivider v-if="index < vmSnapshotVdis.length - 1" class="divider" type="stretch" />
|
||||
</div>
|
||||
</div>
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SnapshotVdiLinkCardItem from '@/modules/snapshot/components/list/panel/card-items/SnapshotVdiLinkCardItem.vue'
|
||||
import SnapshotVdiUsageCardItem from '@/modules/snapshot/components/list/panel/card-items/SnapshotVdiUsageCardItem.vue'
|
||||
import type { FrontXoVmSnapshot } from '@/modules/snapshot/components/remote-resources/use-xo-vm-snapshot-collection.ts'
|
||||
import { useXoVmSnapshotVdiCollection } from '@/modules/snapshot/components/remote-resources/use-xo-vm-snapshot-vdi-collection.ts'
|
||||
import VdiFormatCardItem from '@/modules/vdi/components/list/panel/card-items/VdiFormatCardItem.vue'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
|
||||
import VtsDivider from '@core/components/divider/VtsDivider.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
|
||||
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { snapshot } = defineProps<{ snapshot: FrontXoVmSnapshot }>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { vmSnapshotVdis } = useXoVmSnapshotVdiCollection({}, () => snapshot.id)
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.card-container {
|
||||
gap: 1.6rem;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
|
||||
.divider {
|
||||
margin-block: 1.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { useWatchCollection } from '@/shared/composables/watch-collection.composable.ts'
|
||||
import { useXoCollectionState } from '@/shared/composables/xo-collection-state/use-xo-collection-state.ts'
|
||||
import { BASE_URL } from '@/shared/utils/fetch.util.ts'
|
||||
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
|
||||
import type { XoVmSnapshot } from '@vates/types'
|
||||
|
||||
export type FrontXoVmSnapshot = Pick<XoVmSnapshot, (typeof vmSnapshotFields)[number]>
|
||||
|
||||
const vmSnapshotFields = [
|
||||
'$snapshot_of',
|
||||
'name_label',
|
||||
'name_description',
|
||||
'id',
|
||||
'memory',
|
||||
'snapshot_time',
|
||||
'parent',
|
||||
'power_state',
|
||||
'other',
|
||||
'creation',
|
||||
'$VBDs',
|
||||
] as const satisfies readonly (keyof XoVmSnapshot)[]
|
||||
|
||||
export const useXoVmSnapshotCollection = defineRemoteResource({
|
||||
url: `${BASE_URL}/vm-snapshots?fields=${vmSnapshotFields.join(',')}`,
|
||||
initWatchCollection: () => useWatchCollection({ resource: 'VM-snapshot' }),
|
||||
initialData: () => [] as FrontXoVmSnapshot[],
|
||||
state: (snapshots, context) =>
|
||||
useXoCollectionState(snapshots, {
|
||||
context,
|
||||
baseName: 'snapshot',
|
||||
}),
|
||||
})
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { useWatchCollection } from '@/shared/composables/watch-collection.composable.ts'
|
||||
import { useXoCollectionState } from '@/shared/composables/xo-collection-state/use-xo-collection-state.ts'
|
||||
import { BASE_URL } from '@/shared/utils/fetch.util.ts'
|
||||
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
|
||||
import type { XoVdi } from '@vates/types'
|
||||
|
||||
export type FrontXoVmSnapshotVdi = Pick<XoVdi, (typeof vmSnapshotVdiFields)[number]>
|
||||
|
||||
const vmSnapshotVdiFields = [
|
||||
'id',
|
||||
'name_label',
|
||||
'$VBDs',
|
||||
'image_format',
|
||||
'name_description',
|
||||
'usage',
|
||||
] as const satisfies readonly (keyof XoVdi)[]
|
||||
|
||||
export const useXoVmSnapshotVdiCollection = defineRemoteResource({
|
||||
url: (snapshotId: string) => `${BASE_URL}/vm-snapshots/${snapshotId}/vdis?fields=${vmSnapshotVdiFields.join(',')}`,
|
||||
initWatchCollection: () =>
|
||||
useWatchCollection({
|
||||
collectionId: 'vmSnapshotVdi',
|
||||
resource: 'VDI',
|
||||
}),
|
||||
initialData: () => [] as FrontXoVmSnapshotVdi[],
|
||||
state: (vmSnapshotVdis, context) =>
|
||||
useXoCollectionState(vmSnapshotVdis, {
|
||||
context,
|
||||
baseName: 'vmSnapshotVdi',
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useXoBackupJobCollection } from '@/modules/backup/remote-resources/use-xo-backup-job-collection.ts'
|
||||
import {
|
||||
type FrontXoSchedule,
|
||||
useXoScheduleCollection,
|
||||
} from '@/modules/schedule/remote-resources/use-xo-schedule-collection.ts'
|
||||
import type { FrontXoVmSnapshot } from '@/modules/snapshot/components/remote-resources/use-xo-vm-snapshot-collection.ts'
|
||||
import { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
|
||||
import { icon } from '@core/icons'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export function useSnapshotTrigger() {
|
||||
const { t } = useI18n()
|
||||
|
||||
const { getScheduleById } = useXoScheduleCollection()
|
||||
const { buildXo5Route } = useXoRoutes()
|
||||
const { getBackupJobById } = useXoBackupJobCollection()
|
||||
|
||||
function getSnapshotTrigger(snapshot: FrontXoVmSnapshot) {
|
||||
const scheduleId = snapshot.other?.['xo:backup:schedule'] as FrontXoSchedule['id']
|
||||
|
||||
if (!scheduleId) {
|
||||
return { label: t('manual') }
|
||||
}
|
||||
|
||||
const schedule = getScheduleById(scheduleId)
|
||||
|
||||
const href = buildXo5Route(`/backup/${schedule?.jobId}/edit`)
|
||||
|
||||
const backupJob = getBackupJobById(schedule?.jobId)
|
||||
|
||||
return {
|
||||
label: schedule?.name || t('unnamed'),
|
||||
href,
|
||||
icon: icon('object:backup-schedule'),
|
||||
suffix: backupJob?.name,
|
||||
}
|
||||
}
|
||||
|
||||
return { getSnapshotTrigger }
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import type { FrontXoVmSnapshot } from '@/modules/snapshot/components/remote-resources/use-xo-vm-snapshot-collection.ts'
|
||||
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
|
||||
import { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
|
||||
|
||||
export function useXo5VmSnapshotRoute() {
|
||||
const { buildXo5Route } = useXoRoutes()
|
||||
|
||||
const buildXo5VmSnapshotRoute = (vmId: FrontXoVm['id'], snapshotId: FrontXoVmSnapshot['id']) =>
|
||||
buildXo5Route(`/vms/${vmId}/snapshots?s=1_0_asc-${snapshotId}`)
|
||||
|
||||
return { buildXo5VmSnapshotRoute }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { type FrontXoVbd, useXoVbdCollection } from '@/modules/vbd/remote-resources/use-xo-vbd-collection.ts'
|
||||
import { computed, type ComputedRef, type MaybeRefOrGetter, toValue } from 'vue'
|
||||
|
||||
export type VbdAttachmentStatus = 'allAttached' | 'someAttached' | 'noneAttached'
|
||||
|
||||
export function useVbdsStatus(vbdIds: MaybeRefOrGetter<FrontXoVbd['id'][]>): ComputedRef<VbdAttachmentStatus> {
|
||||
const { getVbdsByIds } = useXoVbdCollection()
|
||||
|
||||
return computed(() => {
|
||||
const vbds = getVbdsByIds(toValue(vbdIds))
|
||||
|
||||
if (vbds.length === 0) {
|
||||
return 'noneAttached'
|
||||
}
|
||||
|
||||
const areAttached = vbds.map(vbd => vbd.attached)
|
||||
|
||||
if (areAttached.every(Boolean)) {
|
||||
return 'allAttached'
|
||||
}
|
||||
|
||||
if (areAttached.some(Boolean)) {
|
||||
return 'someAttached'
|
||||
}
|
||||
|
||||
return 'noneAttached'
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import { BASE_URL } from '@/shared/utils/fetch.util.ts'
|
||||
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
|
||||
import type { XoVbd } from '@vates/types'
|
||||
|
||||
export type FrontXoVbd = Pick<XoVbd, (typeof vbdFields)[number]>
|
||||
|
||||
const vbdFields = [
|
||||
'id',
|
||||
'VDI',
|
||||
@@ -18,7 +20,7 @@ const vbdFields = [
|
||||
export const useXoVbdCollection = defineRemoteResource({
|
||||
url: `${BASE_URL}/vbds?fields=${vbdFields.join(',')}`,
|
||||
initWatchCollection: () => useWatchCollection({ resource: 'VBD', fields: vbdFields }),
|
||||
initialData: () => [] as Pick<XoVbd, (typeof vbdFields)[number]>[],
|
||||
initialData: () => [] as FrontXoVbd[],
|
||||
state: (vbds, context) =>
|
||||
useXoCollectionState(vbds, {
|
||||
context,
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>
|
||||
{{ t('format') }}
|
||||
</template>
|
||||
<template #value>{{ format }}</template>
|
||||
<template v-if="rawFormat" #addons>
|
||||
<VtsCopyButton :value="format" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getVdiFormat } from '@/modules/vdi/utils/xo-vdi.util.ts'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { format: rawFormat } = defineProps<{ format?: string }>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const format = computed(() => getVdiFormat(rawFormat))
|
||||
</script>
|
||||
+2
-14
@@ -4,17 +4,7 @@
|
||||
{{ t('configuration') }}
|
||||
</UiCardTitle>
|
||||
<div class="content">
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>
|
||||
{{ t('format') }}
|
||||
</template>
|
||||
<template #value>
|
||||
{{ format }}
|
||||
</template>
|
||||
<template #addons>
|
||||
<VtsCopyButton :value="format" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VdiFormatCardItem :format="vdi.image_format" />
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>
|
||||
{{ t('storage') }}
|
||||
@@ -47,8 +37,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useXoSrCollection } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
|
||||
import VdiFormatCardItem from '@/modules/vdi/components/list/panel/card-items/VdiFormatCardItem.vue'
|
||||
import type { FrontXoVdi } from '@/modules/vdi/remote-resources/use-xo-vdi-collection.ts'
|
||||
import { getVdiFormat } from '@/modules/vdi/utils/xo-vdi.util.ts'
|
||||
import { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
|
||||
@@ -69,8 +59,6 @@ const { buildXo5Route } = useXoRoutes()
|
||||
|
||||
const { useGetSrById } = useXoSrCollection()
|
||||
|
||||
const format = computed(() => getVdiFormat(vdi.image_format))
|
||||
|
||||
const vdiSr = useGetSrById(() => vdi.$SR)
|
||||
|
||||
const srHref = computed(() => (vdiSr.value ? buildXo5Route(`/srs/${vdiSr.value.id}/general`) : undefined))
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useXoVbdCollection } from '@/modules/vbd/remote-resources/use-xo-vbd-collection.ts'
|
||||
import { useVbdsStatus } from '@/modules/vbd/composables/use-vbds-status.composable.ts'
|
||||
import type { FrontXoVdi } from '@/modules/vdi/remote-resources/use-xo-vdi-collection.ts'
|
||||
import { useXoVmVbdsUtils } from '@/modules/vm/composables/xo-vm-vbd-utils.composable.ts'
|
||||
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
|
||||
@@ -58,6 +58,7 @@ import UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import UiTag from '@core/components/ui/tag/UiTag.vue'
|
||||
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
|
||||
import { useMapper } from '@core/packages/mapper'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -70,28 +71,19 @@ const { t } = useI18n()
|
||||
|
||||
const { buildXo5Route } = useXoRoutes()
|
||||
|
||||
const { getVbdsByIds } = useXoVbdCollection()
|
||||
|
||||
const vdiHref = computed(() => buildXo5Route(`/vms/${vm.id}/disks/s=1_6_asc-${vdi.id}`))
|
||||
|
||||
const vbdsStatus = computed(() => {
|
||||
const vdiVbds = getVbdsByIds(vdi.$VBDs)
|
||||
if (vdiVbds.length === 0) {
|
||||
return CONNECTION_STATUS.DISCONNECTED
|
||||
}
|
||||
const vbdAttachmentStatus = useVbdsStatus(() => vdi.$VBDs)
|
||||
|
||||
const areVdiVbdsAttached = vdiVbds.map(vbd => vbd.attached)
|
||||
|
||||
if (areVdiVbdsAttached.every(Boolean)) {
|
||||
return CONNECTION_STATUS.CONNECTED
|
||||
}
|
||||
|
||||
if (areVdiVbdsAttached.some(Boolean)) {
|
||||
return CONNECTION_STATUS.PARTIALLY_CONNECTED
|
||||
}
|
||||
|
||||
return CONNECTION_STATUS.DISCONNECTED
|
||||
})
|
||||
const vbdsStatus = useMapper(
|
||||
() => vbdAttachmentStatus.value,
|
||||
{
|
||||
allAttached: CONNECTION_STATUS.CONNECTED,
|
||||
someAttached: CONNECTION_STATUS.PARTIALLY_CONNECTED,
|
||||
noneAttached: CONNECTION_STATUS.DISCONNECTED,
|
||||
},
|
||||
'noneAttached'
|
||||
)
|
||||
|
||||
const { notCdDriveVbds } = useXoVmVbdsUtils(() => vm)
|
||||
|
||||
|
||||
@@ -76,6 +76,11 @@
|
||||
{{ t('vdis') }}
|
||||
</TabItem>
|
||||
</RouterLink>
|
||||
<RouterLink v-slot="{ isActive, href }" :to="{ name: `/vm/[id]/snapshots`, params: { id: vm.id } }" custom>
|
||||
<TabItem :active="isActive" :href tag="a">
|
||||
{{ t('snapshots') }}
|
||||
</TabItem>
|
||||
</RouterLink>
|
||||
<RouterLink v-slot="{ isActive, href }" :to="{ name: '/vm/[id]/tasks', params: { id: vm.id } }" custom>
|
||||
<TabItem :active="isActive" :href tag="a">
|
||||
{{ t('tasks') }}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<UiCard class="vm-snapshot-card">
|
||||
<UiCardTitle>{{ title }}</UiCardTitle>
|
||||
<div class="snapshot-row-container">
|
||||
<VtsQuickInfoRow :label="t('snapshot')" class="snapshot-row">
|
||||
<template #value>
|
||||
<UiLink
|
||||
v-if="snapshot?.name_label"
|
||||
icon="object:vm-snapshot"
|
||||
size="medium"
|
||||
:href="buildXo5VmSnapshotRoute(snapshot?.$snapshot_of, snapshot?.id)"
|
||||
>
|
||||
{{ snapshot.name_label }}
|
||||
</UiLink>
|
||||
</template>
|
||||
</VtsQuickInfoRow>
|
||||
<VtsCopyButton v-if="snapshot?.name_label" :value="snapshot.name_label" class="copy-button" />
|
||||
</div>
|
||||
<VtsQuickInfoRow :label="t('snapshot-created-on')">
|
||||
<template #value>
|
||||
<span v-if="formattedDate">{{ formattedDate }}</span>
|
||||
</template>
|
||||
</VtsQuickInfoRow>
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FrontXoVmSnapshot } from '@/modules/snapshot/components/remote-resources/use-xo-vm-snapshot-collection.ts'
|
||||
import { useXo5VmSnapshotRoute } from '@/modules/snapshot/composables/xo-vm-snapshot-route-xo5.composable.ts'
|
||||
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
|
||||
import VtsQuickInfoRow from '@core/components/quick-info-row/VtsQuickInfoRow.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { snapshot } = defineProps<{
|
||||
snapshot: FrontXoVmSnapshot | undefined
|
||||
title: string
|
||||
}>()
|
||||
|
||||
const { t, d } = useI18n()
|
||||
|
||||
const formattedDate = computed(() =>
|
||||
snapshot ? d(snapshot.snapshot_time * 1000, { dateStyle: 'short', timeStyle: 'medium' }) : undefined
|
||||
)
|
||||
|
||||
const { buildXo5VmSnapshotRoute } = useXo5VmSnapshotRoute()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.vm-snapshot-card {
|
||||
.snapshot-row-container {
|
||||
position: relative;
|
||||
|
||||
.snapshot-row {
|
||||
padding-inline-end: 2.5rem;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -56,6 +56,7 @@ const vmFields = [
|
||||
'$VBDs',
|
||||
'snapshots',
|
||||
'boot',
|
||||
'parent',
|
||||
] as const satisfies readonly (keyof XoVm)[]
|
||||
|
||||
export const useXoVmCollection = defineRemoteResource({
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="snapshots" :class="{ mobile: uiStore.isSmall }">
|
||||
<div>
|
||||
<div class="row">
|
||||
<VmSnapshotCard class="last-snapshot" :snapshot="lastSnapshot" :title="t('last-snapshot')" />
|
||||
<VmSnapshotCard class="last-revert" :snapshot="lastRevertSnapshot" :title="t('last-revert')" />
|
||||
</div>
|
||||
<UiCard class="container">
|
||||
<SnapshotsTable :snapshots :vm />
|
||||
</UiCard>
|
||||
</div>
|
||||
<SnapshotSidePanel v-if="selectedSnapshot" :snapshot="selectedSnapshot" @close="selectedSnapshot = undefined" />
|
||||
<UiPanel v-else-if="!uiStore.isSmall">
|
||||
<VtsStateHero format="panel" type="no-selection" size="medium">
|
||||
{{ t('select-to-see-details') }}
|
||||
</VtsStateHero>
|
||||
</UiPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SnapshotSidePanel from '@/modules/snapshot/components/list/panel/SnapshotSidePanel.vue'
|
||||
import {
|
||||
type FrontXoVmSnapshot,
|
||||
useXoVmSnapshotCollection,
|
||||
} from '@/modules/snapshot/components/remote-resources/use-xo-vm-snapshot-collection.ts'
|
||||
import SnapshotsTable from '@/modules/snapshot/components/SnapshotsTable.vue'
|
||||
import VmSnapshotCard from '@/modules/vm/components/snapshot/cards/VmSnapshotCard.vue'
|
||||
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
import UiPanel from '@core/components/ui/panel/UiPanel.vue'
|
||||
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
|
||||
import { useUiStore } from '@core/stores/ui.store.ts'
|
||||
import { useArrayFilter } from '@vueuse/shared'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { vm } = defineProps<{
|
||||
vm: FrontXoVm
|
||||
}>()
|
||||
|
||||
const { snapshots: snapshotsCollection, getSnapshotById } = useXoVmSnapshotCollection()
|
||||
|
||||
const uiStore = useUiStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const snapshots = useArrayFilter(snapshotsCollection, snapshot => snapshot.$snapshot_of === vm.id)
|
||||
|
||||
const selectedSnapshot = useRouteQuery<FrontXoVmSnapshot | undefined>('id', {
|
||||
toData: id => getSnapshotById(id as FrontXoVmSnapshot['id']),
|
||||
toQuery: snapshot => snapshot?.id ?? '',
|
||||
})
|
||||
|
||||
const lastSnapshot = computed(() => {
|
||||
if (snapshots.value.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return snapshots.value.reduce((latest, current) => {
|
||||
return current.snapshot_time > latest.snapshot_time ? current : latest
|
||||
})
|
||||
})
|
||||
|
||||
const lastRevertSnapshot = computed(() => {
|
||||
if (snapshots.value.length === 0 || !vm.parent) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return snapshots.value.find(snapshot => snapshot.id === vm.parent)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.snapshots {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 40rem;
|
||||
|
||||
.container {
|
||||
height: fit-content;
|
||||
margin: 0.8rem;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
margin: 0.8rem;
|
||||
grid-template-columns: repeat(2, minmax(20rem, 1fr));
|
||||
grid-template-areas: 'last-snapshot last-revert';
|
||||
}
|
||||
|
||||
.last-snapshot {
|
||||
grid-area: last-snapshot;
|
||||
}
|
||||
|
||||
.last-revert {
|
||||
grid-area: last-revert;
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Vendored
+1
@@ -59,6 +59,7 @@ declare module 'vue-router/auto-routes' {
|
||||
'/vm/[id]/console': RouteRecordInfo<'/vm/[id]/console', '/vm/:id/console', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'/vm/[id]/dashboard': RouteRecordInfo<'/vm/[id]/dashboard', '/vm/:id/dashboard', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'/vm/[id]/networks': RouteRecordInfo<'/vm/[id]/networks', '/vm/:id/networks', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'/vm/[id]/snapshots': RouteRecordInfo<'/vm/[id]/snapshots', '/vm/:id/snapshots', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'/vm/[id]/system': RouteRecordInfo<'/vm/[id]/system', '/vm/:id/system', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'/vm/[id]/tasks': RouteRecordInfo<'/vm/[id]/tasks', '/vm/:id/tasks', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'/vm/[id]/vdis': RouteRecordInfo<'/vm/[id]/vdis', '/vm/:id/vdis', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
- [REST API] Expose `POST /rest/v0/pools/:id/actions/create_bonded_network` and `POST /rest/v0/pools/:id/actions/create_internal_network` (PR [#9611](https://github.com/vatesfr/xen-orchestra/pull/9611))
|
||||
- [REST API] Added `nbd` param in network creation endpoints (PR [#9611](https://github.com/vatesfr/xen-orchestra/pull/9611))
|
||||
- [VM] Add backup replication card on dashboard (PR [#9545](https://github.com/vatesfr/xen-orchestra/pull/9545))
|
||||
- [VM/Snapshot] Add VM snapshot tab page (PR [#9521] (https://github.com/vatesfr/xen-orchestra/pull/9521))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user