feat(lite): add enable and disable actions on host (#10337)

* update changelog file

* feedback one

* feat(host): add action reboot on host

* add header state

* feat(lite): add forget action on host

* feat(lite): add enable and disable actions on host

* update changelog file

* fix rebase

* fix rebase

* feedback one

* add redirect after forget action
This commit is contained in:
Elise-F
2026-10-06 13:55:14 +02:00
committed by GitHub
parent cfdf0de9ee
commit 3a64afb226
17 changed files with 226 additions and 9 deletions
+2 -1
View File
@@ -5,7 +5,8 @@
- [VM/System] Fix video RAM displayed in bytes instead of MiB (PR [#10486](https://github.com/vatesfr/xen-orchestra/pull/10486))
- [Treeview] Add a search bar and virtualize the tree (PR [#10414](https://github.com/vatesfr/xen-orchestra/pull/10414))
- [New/VM] Hide guest tools ISO SR in new VM ISO selector (PR [#10430](https://github.com/vatesfr/xen-orchestra/pull/10430))
-
- [Host] Add possibility to enable/disable a host (PR [#10337](https://github.com/vatesfr/xen-orchestra/pull/10337))
## **0.26.0** (2026-10-01)
- [Host] Add possibility to reboot a host (PR [#10250](https://github.com/vatesfr/xen-orchestra/pull/10250))
@@ -20,7 +20,6 @@
</template>
<HostPowerStateActions :host />
</MenuList>
<MenuList placement="bottom-end">
<template #trigger="{ open }">
<UiButtonIcon
@@ -34,6 +34,8 @@ export function createHostOperations(xenApi: XenApi) {
}
return {
disable,
enable,
powerOn,
destroy,
cleanReboot: async (hostRef: HostRef, forceReboot: boolean) => {
@@ -91,6 +91,7 @@ export enum ON_NORMAL_EXIT {
}
export enum HOST_OPERATION {
ENABLE = 'enable',
EVACUATE = 'evacuate',
POWER_ON = 'power_on',
REBOOT = 'reboot',
@@ -5,6 +5,8 @@
<HostPowerStateActions :host />
</template>
</MenuItem>
<HostDisableButton v-if="displayDisableButton" :host />
<HostEnableButton v-else :host />
<HostForgetButton :host />
<VtsDivider type="stretch" />
<HostDownloadButton :host-opaque-ref="host.$ref" />
@@ -12,17 +14,28 @@
<script setup lang="ts">
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import HostDisableButton from '@/modules/host/components/actions/disable/HostDisableButton.vue'
import HostDownloadButton from '@/modules/host/components/actions/download/HostDownloadButton.vue'
import HostEnableButton from '@/modules/host/components/actions/enable/HostEnableButton.vue'
import HostForgetButton from '@/modules/host/components/actions/forget/HostForgetButton.vue'
import HostPowerStateActions from '@/modules/host/components/actions/HostPowerStateActions.vue'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import VtsDivider from '@core/components/divider/VtsDivider.vue'
import MenuItem from '@core/components/menu/MenuItem.vue'
import { HOST_POWER_STATE } from '@vates/types'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
defineProps<{
const { host } = defineProps<{
host: XenApiHost
showChangeStateButton?: boolean
}>()
const { t } = useI18n()
const { getHostPowerState } = useHostMetricsStore().subscribe()
const isHostHalted = computed(() => getHostPowerState(host) === HOST_POWER_STATE.HALTED)
const displayDisableButton = computed(() => host.enabled || isHostHalted.value)
</script>
@@ -0,0 +1,53 @@
<template>
<MenuItem
v-tooltip="!canDisableHost && disableHostErrorMessage"
accent="neutral"
:busy="isDisablingHost"
:disabled="!canDisableHost"
icon="action:disable"
@click="disableHost()"
>
{{ t('action:disable') }}
</MenuItem>
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { useHostDisableJob } from '@/modules/host/jobs/host-disable.job.ts'
import MenuItem from '@core/components/menu/MenuItem.vue'
import { useActionModal } from '@core/composables/modals/use-action-modal.ts'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useI18n } from 'vue-i18n'
const { host } = defineProps<{
host: XenApiHost
}>()
const { t } = useI18n()
const { open: openActionModal } = useActionModal()
const {
run,
canRun: canDisableHost,
isRunning: isDisablingHost,
errorMessage: disableHostErrorMessage,
} = useHostDisableJob(() => host)
function disableHost() {
return openActionModal({
props: {
accent: 'info',
action: 'disable',
object: 'host',
hostName: host.name_label,
icon: 'status:info-picto',
},
events: {
onConfirm: async () => {
void run()
},
},
})
}
</script>
@@ -0,0 +1,53 @@
<template>
<MenuItem
v-tooltip="!canEnableHost && enableHostErrorMessage"
accent="neutral"
:busy="isEnablingHost"
:disabled="!canEnableHost"
icon="status:success-circle"
@click="enableHost()"
>
{{ t('action:enable') }}
</MenuItem>
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { useHostEnableJob } from '@/modules/host/jobs/host-enable.job.ts'
import MenuItem from '@core/components/menu/MenuItem.vue'
import { useActionModal } from '@core/composables/modals/use-action-modal.ts'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useI18n } from 'vue-i18n'
const { host } = defineProps<{
host: XenApiHost
}>()
const { t } = useI18n()
const { open: openActionModal } = useActionModal()
const {
run,
canRun: canEnableHost,
isRunning: isEnablingHost,
errorMessage: enableHostErrorMessage,
} = useHostEnableJob(() => host)
function enableHost() {
return openActionModal({
props: {
accent: 'info',
action: 'enable',
object: 'host',
hostName: host.name_label,
icon: 'status:info-picto',
},
events: {
onConfirm: async () => {
void run()
},
},
})
}
</script>
@@ -14,10 +14,13 @@
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { useHostForgetJob } from '@/modules/host/jobs/host-forget.job.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import MenuItem from '@core/components/menu/MenuItem.vue'
import { useActionModal } from '@core/composables/modals/use-action-modal.ts'
import { useRedirectAfterDelete } from '@core/composables/redirect-after-delete.composable.ts'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
const { host } = defineProps<{
host: XenApiHost
@@ -25,6 +28,10 @@ const { host } = defineProps<{
const { t } = useI18n()
const route = useRoute<'/host/[uuid]'>()
const { pool } = usePoolStore().subscribe()
const { open: openActionModal } = useActionModal()
const {
@@ -34,6 +41,17 @@ const {
errorMessage: forgetHostErrorMessage,
} = useHostForgetJob(() => host)
const { redirect: redirectAfterForgetHost } = useRedirectAfterDelete({
isOnObjectPage: () => route.params.uuid === host.uuid,
redirectTo: () => {
if (pool.value === undefined) {
return { name: '/' }
}
return { name: '/pool/[uuid]/hosts', params: { uuid: pool.value.uuid } }
},
})
function forgetHost() {
return openActionModal({
props: {
@@ -45,7 +63,14 @@ function forgetHost() {
},
events: {
onConfirm: async () => {
void run()
try {
await run()
} catch (error) {
console.error('Error when forgetting host:', error)
return
}
await redirectAfterForgetHost()
},
},
})
@@ -27,6 +27,7 @@ export function useHostUtils(rawHost: MaybeRefOrGetter<XenApiHost | undefined>)
const currentOperation = useMapper<string, string>(
() => pendingStateOperation.value,
{
[HOST_OPERATION.ENABLE]: t('operation:enable'),
[HOST_OPERATION.EVACUATE]: t('operation:evacuate'),
[HOST_OPERATION.POWER_ON]: t('operation:start'),
[HOST_OPERATION.REBOOT]: t('operation:clean-reboot'),
@@ -0,0 +1,35 @@
import { HOST_OPERATION } from '@/libs/xen-api/xen-api.enums.ts'
import { hostArg } from '@/modules/host/jobs/host-args.ts'
import { isHostOperationPending } from '@/modules/host/utils/host.util.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { useXenApiStore } from '@/stores/xen-api.store.ts'
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
import { useI18n } from 'vue-i18n'
export const useHostDisableJob = defineJob('host.disable', [hostArg], () => {
const xapi = useXenApiStore().getXapi()
const { t } = useI18n()
const { isHostRunning } = useHostMetricsStore().subscribe()
return {
run: host => xapi.host.disable(host.$ref),
validate: (isRunning, host) => {
if (host === undefined) {
throw new JobError(t('job:host-disable:missing-host'))
}
if (isRunning) {
throw new JobRunningError(t('job:disable:in-progress'))
}
if (isHostOperationPending(host, HOST_OPERATION.EVACUATE)) {
throw new JobRunningError(t('job:host-evacuate:in-progress'))
}
if (!isHostRunning(host)) {
throw new JobError(t('job:host-disable:bad-power-state'))
}
},
}
})
@@ -0,0 +1,29 @@
import { HOST_OPERATION } from '@/libs/xen-api/xen-api.enums.ts'
import { hostArg } from '@/modules/host/jobs/host-args.ts'
import { isHostOperationPending } from '@/modules/host/utils/host.util.ts'
import { useXenApiStore } from '@/stores/xen-api.store.ts'
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
import { useI18n } from 'vue-i18n'
export const useHostEnableJob = defineJob('host.enable', [hostArg], () => {
const xapi = useXenApiStore().getXapi()
const { t } = useI18n()
return {
run: host => xapi.host.enable(host.$ref),
validate: (isRunning, host) => {
if (host === undefined) {
throw new JobError(t('job:host-enable:missing-host'))
}
if (isRunning || isHostOperationPending(host, HOST_OPERATION.ENABLE)) {
throw new JobRunningError(t('job:enable:in-progress'))
}
if (isHostOperationPending(host, HOST_OPERATION.EVACUATE)) {
throw new JobRunningError(t('job:host-evacuate:in-progress'))
}
},
}
})
@@ -2,7 +2,12 @@ import { HOST_OPERATION } from '@/libs/xen-api/xen-api.enums.ts'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { castArray } from 'lodash-es'
const RUNNING_CHANGING_STATE_OPERATIONS = [HOST_OPERATION.EVACUATE, HOST_OPERATION.REBOOT, HOST_OPERATION.SHUTDOWN]
const RUNNING_CHANGING_STATE_OPERATIONS = [
HOST_OPERATION.ENABLE,
HOST_OPERATION.EVACUATE,
HOST_OPERATION.REBOOT,
HOST_OPERATION.SHUTDOWN,
]
const NOT_RUNNING_CHANGING_STATE_OPERATIONS = [HOST_OPERATION.POWER_ON]
@@ -15,9 +15,9 @@
import { useXoHostDetachJob } from '@/modules/host/jobs/xo-host-detach.job.ts'
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPoolCollection } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
import { useRedirectAfterDelete } from '@/shared/composables/redirect-after-delete.composable.ts'
import MenuItem from '@core/components/menu/MenuItem.vue'
import { useActionModal } from '@core/composables/modals/use-action-modal.ts'
import { useRedirectAfterDelete } from '@core/composables/redirect-after-delete.composable.ts'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
@@ -15,9 +15,9 @@
import { useXoHostForgetJob } from '@/modules/host/jobs/xo-host-forget.job.ts'
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPoolCollection } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
import { useRedirectAfterDelete } from '@/shared/composables/redirect-after-delete.composable.ts'
import MenuItem from '@core/components/menu/MenuItem.vue'
import { useActionModal } from '@core/composables/modals/use-action-modal.ts'
import { useRedirectAfterDelete } from '@core/composables/redirect-after-delete.composable.ts'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
@@ -1,7 +1,7 @@
import { useXoVifDeleteJob } from '@/modules/vif/jobs/xo-vif-delete.job.ts'
import type { FrontXoVif } from '@/modules/vif/remote-resources/use-xo-vif-collection.ts'
import { useRedirectAfterDelete } from '@/shared/composables/redirect-after-delete.composable.ts'
import { useDeleteModal } from '@core/composables/modals/use-delete-modal.ts'
import { useRedirectAfterDelete } from '@core/composables/redirect-after-delete.composable.ts'
import { toComputed } from '@core/utils/to-computed.util.ts'
import type { MaybeRefOrGetter } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -9,10 +9,10 @@ import { useXoVmUtils } from '@/modules/vm/composables/xo-vm-utils.composable.ts
import { useXoVmDeleteJob } from '@/modules/vm/jobs/xo-vm-delete.job.ts'
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import { extractVmHostId } from '@/modules/vm/utils/xo-vm.util.ts'
import { useRedirectAfterDelete } from '@/shared/composables/redirect-after-delete.composable.ts'
import MenuItem from '@core/components/menu/MenuItem.vue'
import { useBlockedModal } from '@core/composables/modals/use-blocked-modal.ts'
import { useDeleteModal } from '@core/composables/modals/use-delete-modal.ts'
import { useRedirectAfterDelete } from '@core/composables/redirect-after-delete.composable.ts'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'