feat(xo-lite): add vm tree actions and loader (#10304)

This commit is contained in:
Joris K
2026-08-27 10:56:34 +02:00
committed by GitHub
parent dfb4c925ec
commit 2ebf18a636
11 changed files with 158 additions and 8 deletions

View File

@@ -2,6 +2,7 @@
## **next**
- [Treeview] Add VM tree actions (PR [#10304](https://github.com/vatesfr/xen-orchestra/pull/10304))
- [Pool/networks] Add the possibility to create new network or bonded network (PR [#10145](https://github.com/vatesfr/xen-orchestra/pull/10145))
- [VM] Add VDIs page with table and side panel (PR [#10269](https://github.com/vatesfr/xen-orchestra/pull/10269))
- [Host/Network] Add ability to rescan physical network interfaces (PIFs) (PR [#10147](https://github.com/vatesfr/xen-orchestra/pull/10147))

View File

@@ -5,6 +5,24 @@
<template #icon>
<VtsObjectIcon size="medium" :state="vmPowerState!" type="vm" />
</template>
<template #addons>
<UiLoader v-if="isChangingState" v-tooltip="{ placement: 'top', content: currentOperation }" />
<MenuList placement="bottom-start">
<template #trigger="{ open }">
<UiButtonIcon
v-tooltip="{
placement: 'top',
content: t('quick-actions'),
}"
icon="action:more-actions"
accent="brand"
size="small"
@click="open($event)"
/>
</template>
<VmTreeActions :vm-opaque-ref />
</MenuList>
</template>
</UiTreeItemLabel>
</VtsTreeItem>
</template>
@@ -12,17 +30,26 @@
<script lang="ts" setup>
import type { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums.ts'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
import VmTreeActions from '@/modules/vm/components/actions/VmTreeActions.vue'
import { useVmOperation } from '@/modules/vm/composables/vm-operation.composable.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import MenuList from '@core/components/menu/MenuList.vue'
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
import VtsTreeItem from '@core/components/tree/VtsTreeItem.vue'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
import UiLoader from '@core/components/ui/loader/UiLoader.vue'
import UiTreeItemLabel from '@core/components/ui/tree-item-label/UiTreeItemLabel.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useIntersectionObserver } from '@vueuse/core'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { vmOpaqueRef } = defineProps<{
vmOpaqueRef: XenApiVm['$ref']
}>()
const { t } = useI18n()
const { getByOpaqueRef } = useVmStore().subscribe()
const vm = computed(() => getByOpaqueRef(vmOpaqueRef))
const rootElement = ref()
@@ -36,4 +63,6 @@ const { stop } = useIntersectionObserver(rootElement, ([entry]) => {
})
const vmPowerState = computed(() => vm.value?.power_state.toLowerCase() as Lowercase<VM_POWER_STATE> | undefined)
const { isChangingState, currentOperation } = useVmOperation(vm)
</script>

View File

@@ -2,7 +2,13 @@
<UiHeadBar icon="object:vm">
{{ vm?.name_label }}
<template #icon>
<VtsObjectIcon size="medium" :state="toLower(vm?.power_state)" type="vm" />
<VtsObjectIcon
size="medium"
:state="toLower(vm?.power_state)"
type="vm"
:busy="isChangingState"
:busy-tooltip="currentOperation"
/>
</template>
<template #actions>
<MenuList v-if="vm !== undefined" placement="bottom-start">
@@ -40,6 +46,7 @@ import VmActionMigrateItem from '@/components/vm/VmActionItems/VmActionMigrateIt
import VmActionPowerStateItems from '@/components/vm/VmActionItems/VmActionPowerStateItems.vue'
import VmActionSnapshotItem from '@/components/vm/VmActionItems/VmActionSnapshotItem.vue'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
import { useVmOperation } from '@/modules/vm/composables/vm-operation.composable.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import MenuList from '@core/components/menu/MenuList.vue'
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
@@ -58,4 +65,6 @@ const { getByUuid: getVmByUuid } = useVmStore().subscribe()
const route = useRoute<'/vm/[uuid]'>()
const vm = computed(() => getVmByUuid(route.params.uuid as XenApiVm['uuid']))
const { isChangingState, currentOperation } = useVmOperation(vm)
</script>

View File

@@ -0,0 +1,40 @@
<template>
<MenuItem class="change-state" icon="action:change-state">
{{ t('action:change-state') }}
<template #submenu>
<VmActionPowerStateItems :vm-refs />
</template>
</MenuItem>
<VmActionSnapshotItem :vm-refs />
<VmActionCopyItem :selected-refs="vmRefs" is-single-action />
<VmActionExportItem :vm-refs is-single-action />
<MenuSeparator />
<VmActionDeleteItem :vm-refs />
</template>
<script setup lang="ts">
import VmActionCopyItem from '@/components/vm/VmActionItems/VmActionCopyItem.vue'
import VmActionDeleteItem from '@/components/vm/VmActionItems/VmActionDeleteItem.vue'
import VmActionExportItem from '@/components/vm/VmActionItems/VmActionExportItem.vue'
import VmActionPowerStateItems from '@/components/vm/VmActionItems/VmActionPowerStateItems.vue'
import VmActionSnapshotItem from '@/components/vm/VmActionItems/VmActionSnapshotItem.vue'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
import MenuItem from '@core/components/menu/MenuItem.vue'
import MenuSeparator from '@core/components/menu/MenuSeparator.vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { vmOpaqueRef } = defineProps<{
vmOpaqueRef: XenApiVm['$ref']
}>()
const { t } = useI18n()
const vmRefs = computed(() => [vmOpaqueRef])
</script>
<style lang="postcss" scoped>
.change-state {
color: var(--color-brand-txt-base);
}
</style>

View File

@@ -0,0 +1,46 @@
import { isVmOperationPending } from '@/libs/vm.ts'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
import { CHANGING_STATE_OPERATIONS } from '@/modules/vm/utils/vm.util.ts'
import { useMapper } from '@core/packages/mapper'
import { computed, type MaybeRefOrGetter, toValue } from 'vue'
import { useI18n } from 'vue-i18n'
export function useVmOperation(rawVm: MaybeRefOrGetter<XenApiVm | undefined>) {
const { t } = useI18n()
const vm = toValue(rawVm)
const isChangingState = computed(() => {
return vm !== undefined && isVmOperationPending(vm, CHANGING_STATE_OPERATIONS)
})
const currentOperation = useMapper<string, string>(
() => Object.values(vm?.current_operations ?? {})[0],
{
start: t('operation:start'),
start_on: t('operation:start-on-host'),
pause: t('operation:pause'),
unpause: t('operation:unpause'),
resume: t('operation:resume'),
suspend: t('operation:suspend'),
clean_reboot: t('operation:clean-reboot'),
hard_reboot: t('operation:force-reboot'),
shutdown: t('operation:shutdown'),
clean_shutdown: t('operation:clean-shutdown'),
hard_shutdown: t('operation:force-shutdown'),
snapshot: t('operation:snapshot'),
destroy: t('operation:destroy'),
clone: t('operation:duplicate'),
copy: t('operation:duplicate'),
export: t('operation:export'),
import: t('operation:import'),
unknown: '',
},
'unknown'
)
return {
isChangingState,
currentOperation,
}
}

View File

@@ -1,5 +1,26 @@
import { VM_OPERATION } from '@/libs/xen-api/xen-api.enums.ts'
import type { XenApiVmGuestMetrics } from '@/libs/xen-api/xen-api.types.ts'
export const CHANGING_STATE_OPERATIONS: VM_OPERATION[] = [
VM_OPERATION.START,
VM_OPERATION.START_ON,
VM_OPERATION.PAUSE,
VM_OPERATION.UNPAUSE,
VM_OPERATION.RESUME,
VM_OPERATION.SUSPEND,
VM_OPERATION.CLEAN_REBOOT,
VM_OPERATION.HARD_REBOOT,
VM_OPERATION.SHUTDOWN,
VM_OPERATION.CLEAN_SHUTDOWN,
VM_OPERATION.HARD_SHUTDOWN,
VM_OPERATION.SNAPSHOT,
VM_OPERATION.DESTROY,
VM_OPERATION.CLONE,
VM_OPERATION.COPY,
VM_OPERATION.EXPORT,
VM_OPERATION.IMPORT,
]
/**
* Adapted from `getVmGuestToolsProps` in xo-server (`packages/xo-server/src/xapi-object-to-xo.mjs`),
* which computes `managementAgentDetected` and `pvDriversDetected` on XO VM objects.

View File

@@ -1,5 +1,5 @@
<template>
<VtsIcon :name="iconName" :size />
<VtsIcon :name="iconName" :size :busy :busy-tooltip />
</template>
<script generic="TType extends ObjectType, TState extends ObjectState<TType>" lang="ts" setup>
@@ -11,6 +11,8 @@ const { type, state } = defineProps<{
type: TType
state?: TState
size: IconSize
busy?: boolean
busyTooltip?: string
}>()
const iconName = computed(() => (state ? objectIcon(type, state) : icon(`object:${type}`)))

View File

@@ -848,8 +848,10 @@
"operation:duplicate": "Duplicating",
"operation:enable": "Enabling",
"operation:evacuate": "Evacuating",
"operation:export": "Exporting",
"operation:force-reboot": "Force rebooting",
"operation:force-shutdown": "Force shutting down",
"operation:import": "Importing",
"operation:pause": "Pausing",
"operation:resume": "Resuming",
"operation:shutdown": "Shutting down",

View File

@@ -848,8 +848,10 @@
"operation:duplicate": "Duplication",
"operation:enable": "Activation",
"operation:evacuate": "Évacuation",
"operation:export": "Exportation",
"operation:force-reboot": "Redémarrage forcé",
"operation:force-shutdown": "Arrêt forcé",
"operation:import": "Importation",
"operation:pause": "Mise en pause",
"operation:resume": "Reprise",
"operation:shutdown": "Arrêt",

View File

@@ -3,14 +3,11 @@
{{ vm.name_label }}
<template #icon>
<VtsObjectIcon
v-tooltip="{
placement: 'top',
content: currentOperation ? currentOperation : '',
}"
size="medium"
:state="toLower(vm.power_state)"
type="vm"
:busy="isChangingState"
:busy-tooltip="currentOperation"
/>
</template>
<template #actions>

View File

@@ -114,14 +114,15 @@ export function useXoVmUtils(rawVm: MaybeRefOrGetter<FrontXoVm>) {
resume: t('operation:resume'),
clean_reboot: t('operation:clean-reboot'),
hard_reboot: t('operation:force-reboot'),
shutdown: t('operation:shutdown'),
clean_shutdown: t('operation:clean-shutdown'),
hard_shutdown: t('operation:force-shutdown'),
destroy: t('operation:destroy'),
snapshot: t('operation:snapshot'),
clone: t('operation:duplicate'),
copy: t('operation:duplicate'),
export: t('operation:duplicate'),
import: t('operation:duplicate'),
export: t('operation:export'),
import: t('operation:import'),
unknown: '',
},
'unknown'