feat(xo6/pool): add connect/disconnect actions on page site (#10075)

This commit is contained in:
Alina D
2026-07-27 13:26:20 +02:00
committed by GitHub
parent bcb91e8b4f
commit 246e2e4eba
15 changed files with 261 additions and 58 deletions

View File

@@ -498,6 +498,9 @@
"job:pbd-plug:pbd-connected": "One or several PBDs are already connected",
"job:pbd-unplug:missing-pbd": "No PBDs to disconnect",
"job:pbd-unplug:pbd-disconnected": "One or several PBDs are already disconnected",
"job:server-connect:missing-server": "No servers to connect",
"job:server-disconnect:missing-server": "No servers to disconnect",
"job:server-disconnect:server-disconnected": "One or several servers are already disconnected",
"job:snapshot-delete:missing-snapshot": "No snapshots to delete",
"job:sr-connect:missing-sr": "No SRs to connect",
"job:sr-delete:missing-sr": "No SRs to delete",
@@ -643,6 +646,7 @@
"modal:confirm-vm-shutdown": "Confirm VM shutdown",
"modal:host-disable-message": "The host will be marked unavailable for starting or receiving new VMs. Already-running VMs are not affected.",
"modal:host-enable-message": "The host becomes available again for starting or receiving VMs.",
"modal:pool-disconnect-message": "The pool will no longer be manageable from XO. You can reconnect it later.",
"modal:vm-force-reboot-message": "This will immediately reboot the virtual machine without a clean shutdown. Data loss may occur and ongoing processes will be interrupted.",
"modal:vm-force-shutdown-message": "This will immediately power off the virtual machine without a clean shutdown. Data loss may occur and ongoing processes will be interrupted.",
"modal:vm-reboot-message": "Restarts the virtual machine gracefully by shutting it down and starting it again. Running processes will be closed properly.",
@@ -832,7 +836,6 @@
"pool-connection-ip-info": "Provide the primary hosts address of the pool you want to connect to.",
"pool-connection-success": "You have successfully connected to the pool. You can now find it in your treeview and start managing your hosts and VMs.",
"pool-cpu-usage": "Pool CPU Usage",
"pool-disconnect-info": "You will no longer be able to manage this pool from XO. You can reconnect it later.",
"pool-management": "Pool management",
"pool-ram-usage": "Pool RAM Usage",
"pool:status:connected": "@:status:connected",

View File

@@ -498,6 +498,9 @@
"job:pbd-plug:pbd-connected": "Un ou plusieurs PBDs sont déjà connectés",
"job:pbd-unplug:missing-pbd": "Aucun PBD à déconnecter",
"job:pbd-unplug:pbd-disconnected": "Un ou plusieurs PBDs sont déjà déconnectés",
"job:server-connect:missing-server": "Aucun serveur à connecter",
"job:server-disconnect:missing-server": "Aucun serveur à déconnecter",
"job:server-disconnect:server-disconnected": "Un ou plusieurs serveurs sont déjà déconnectés",
"job:snapshot-delete:missing-snapshot": "Aucun instantané à supprimer",
"job:sr-connect:missing-sr": "Aucun SR à connecter",
"job:sr-delete:missing-sr": "Aucun SR à supprimer",
@@ -643,6 +646,7 @@
"modal:confirm-vm-shutdown": "Confirmer l'arrêt de la VM",
"modal:host-disable-message": "Lhôte sera marqué comme indisponible pour le démarrage ou la réception de nouvelles VMs. Les VMs déjà en cours dexécution ne sont pas affectées.",
"modal:host-enable-message": "Lhôte redevient disponible pour démarrer ou recevoir des VMs.",
"modal:pool-disconnect-message": "Le pool ne pourra plus être géré depuis XO. Vous pourrez le reconnecter ultérieurement.",
"modal:vm-force-reboot-message": "Cela redémarrera immédiatement la VM sans arrêt propre. Des pertes de données peuvent survenir et les processus en cours seront interrompus.",
"modal:vm-force-shutdown-message": "Cela éteindra immédiatement la VM sans arrêt propre. Des pertes de données peuvent survenir et les processus en cours seront interrompus.",
"modal:vm-reboot-message": "Redémarre la VM de manière propre en l'éteignant puis en la redémarrant. Les processus en cours seront fermés correctement.",
@@ -832,7 +836,6 @@
"pool-connection-ip-info": "Entrez ladresse de lhôte primaire du pool auquel vous souhaitez vous connecter.",
"pool-connection-success": "Vous êtes connecté au pool. Vous pouvez désormais le retrouver dans votre arborescence et commencer à gérer vos hôtes et vos machines virtuelles.",
"pool-cpu-usage": "Utilisation CPU du Pool",
"pool-disconnect-info": "Vous ne pourrez plus gérer ce pool depuis XO. Vous pourrez le reconnecter ultérieurement.",
"pool-management": "Gestion du Pool",
"pool-ram-usage": "Utilisation RAM du Pool",
"pool:status:connected": "@:status:connected",

View File

@@ -1,7 +1,7 @@
import { defineColumns } from '@core/packages/table/define-columns.ts'
import { useActionColumn } from '@core/tables/column-definitions/action-column.ts'
import { useAddressColumn } from '@core/tables/column-definitions/address-column.ts'
import { useLinkColumn } from '@core/tables/column-definitions/link-column'
import { useSelectItemColumn } from '@core/tables/column-definitions/select-item-column'
import { useStatusColumn } from '@core/tables/column-definitions/status-column.ts'
import { useI18n } from 'vue-i18n'
@@ -13,6 +13,6 @@ export const useServerColumns = defineColumns(() => {
hostIp: useAddressColumn({ headerLabel: () => t('ip-address') }),
status: useStatusColumn({ headerLabel: () => t('status') }),
primaryHost: useLinkColumn({ headerLabel: () => t('master') }),
selectItem: useSelectItemColumn(),
actions: useActionColumn({}),
}
})

View File

@@ -1,6 +1,6 @@
<template>
<PoolNewVmButton :pool-id="pool.id" />
<PoolDisconnectButton :pool-id="pool.id" :pool-name="pool.name_label" />
<PoolDisconnectButton :pool-id="pool.id" />
<VtsDivider type="stretch" />
<PoolDownloadButton :pool-id="pool.id" />
</template>

View File

@@ -0,0 +1,75 @@
<template>
<template v-if="server">
<UiButton
v-if="server.status === 'connected'"
v-tooltip="!canDisconnect ? disconnectErrorMessage : undefined"
left-icon="action:disconnect"
variant="tertiary"
accent="danger"
size="medium"
:disabled="!canDisconnect"
:busy="isDisconnecting"
@click="openDisconnectModal()"
>
{{ t('action:disconnect-pool') }}
</UiButton>
<UiButton
v-else
v-tooltip="!canConnect ? connectErrorMessage : undefined"
left-icon="action:connect"
variant="tertiary"
accent="brand"
size="medium"
:disabled="!canConnect"
:busy="isConnecting"
@click="handleConnect()"
>
{{ t('action:connect-pool') }}
</UiButton>
</template>
</template>
<script lang="ts" setup>
import { useServerDisconnectModal } from '@/modules/server/composables/use-server-disconnect-modal.composable.ts'
import { useXoServerConnectJob } from '@/modules/server/jobs/xo-server-connect.job.ts'
import {
type FrontXoServer,
useXoServerCollection,
} from '@/modules/server/remote-resources/use-xo-server-collection.ts'
import UiButton from '@core/components/ui/button/UiButton.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { serverId } = defineProps<{ serverId: FrontXoServer['id'] }>()
const { t } = useI18n()
const { getServerById } = useXoServerCollection()
const server = computed(() => getServerById(serverId))
const serverIdArg = computed(() => serverId)
const {
isRunning: isConnecting,
canRun: canConnect,
errorMessage: connectErrorMessage,
run: connect,
} = useXoServerConnectJob([serverIdArg])
const {
openModal: openDisconnectModal,
canRun: canDisconnect,
isRunning: isDisconnecting,
errorMessage: disconnectErrorMessage,
} = useServerDisconnectModal(() => serverId)
async function handleConnect() {
try {
await connect()
} catch (error) {
console.error('Error when connecting server:', error)
}
}
</script>

View File

@@ -12,36 +12,22 @@
<script lang="ts" setup>
import type { FrontXoPool } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
import { useXoServerDisconnectJob } from '@/modules/server/jobs/xo-server-disconnect.job.ts'
import { useServerDisconnectModal } from '@/modules/server/composables/use-server-disconnect-modal.composable.ts'
import { useXoServerCollection } from '@/modules/server/remote-resources/use-xo-server-collection.ts'
import MenuItem from '@core/components/menu/MenuItem.vue'
import { useModal } from '@core/packages/modal/use-modal.ts'
import type { XoServer } from '@vates/types'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { poolId, poolName } = defineProps<{ poolId: FrontXoPool['id']; poolName: string }>()
const { poolId } = defineProps<{ poolId: FrontXoPool['id'] }>()
const { t } = useI18n()
const { serverByPool } = useXoServerCollection()
const server = computed(() => serverByPool.value.get(poolId)?.[0])
const serverId = computed(() => server.value?.id ?? ('' as XoServer['id']))
const serverId = computed(() => serverByPool.value.get(poolId)?.[0]?.id ?? ('' as XoServer['id']))
const { canRun, isRunning, run } = useXoServerDisconnectJob([serverId])
const openModal = useModal(() => ({
component: import('@core/components/modal/VtsDisconnectModal.vue'),
props: { title: poolName },
onConfirm: async () => {
try {
await run()
} catch (error) {
console.error('Error when disconnecting server:', error)
}
},
}))
const { openModal, canRun, isRunning } = useServerDisconnectModal(serverId)
</script>
<style lang="postcss" scoped>

View File

@@ -1,36 +1,18 @@
<template>
<MenuItem
icon="action:download"
class="typo-body-bold-small"
:busy="areHostsFetching"
:disabled="(areHostsReady && primaryHost === undefined) || hasHostFetchError"
@click="download()"
>
<MenuItem icon="action:download" class="typo-body-bold-small" :busy="isBusy" :disabled="isDisabled" @click="download">
{{ t('action:download-bugtools-archive') }}
</MenuItem>
</template>
<script lang="ts" setup>
import { useXoHostCollection } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { usePoolBugToolsDownload } from '@/modules/pool/composables/use-pool-bugtools-download.composable.ts'
import type { FrontXoPool } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
import MenuItem from '@core/components/menu/MenuItem.vue'
import { downloadBugTools } from '@core/utils/download-bugtools.utils.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { poolId } = defineProps<{ poolId: FrontXoPool['id'] }>()
const { t } = useI18n()
const { getMasterHostByPoolId, areHostsFetching, areHostsReady, hasHostFetchError } = useXoHostCollection()
const primaryHost = computed(() => getMasterHostByPoolId(poolId))
const download = () => {
if (primaryHost.value?.address === undefined) {
return
}
downloadBugTools(primaryHost.value.address)
}
const { download, isBusy, isDisabled } = usePoolBugToolsDownload(() => poolId)
</script>

View File

@@ -22,14 +22,17 @@
</template>
<script setup lang="ts">
import { usePoolBugToolsDownload } from '@/modules/pool/composables/use-pool-bugtools-download.composable.ts'
import {
usePoolEnhancedData,
type PoolDisplayData,
type PoolFilterableData,
usePoolEnhancedData,
} from '@/modules/pool/composables/use-pool-enhanced-data.composable.ts'
import { useServerDisconnectModal } from '@/modules/server/composables/use-server-disconnect-modal.composable.ts'
import { useXoServerConnectJob } from '@/modules/server/jobs/xo-server-connect.job.ts'
import {
useXoServerCollection,
type FrontXoServer,
useXoServerCollection,
} from '@/modules/server/remote-resources/use-xo-server-collection.ts'
import VtsQueryBuilder from '@core/components/query-builder/VtsQueryBuilder.vue'
import VtsRow from '@core/components/table/VtsRow.vue'
@@ -104,6 +107,30 @@ const { HeadCells, BodyCells } = useServerColumns({
icon: server.poolIcon,
}))
const serverIdArg = computed(() => server.id)
const { canRun: canConnect, isRunning: isConnecting, run: connect } = useXoServerConnectJob([serverIdArg])
const {
openModal: openDisconnectModal,
canRun: canDisconnect,
isRunning: isDisconnecting,
} = useServerDisconnectModal(() => server.id)
const {
download: downloadBugTools,
isBusy: isDownloadBusy,
isDisabled: isDownloadDisabled,
} = usePoolBugToolsDownload(() => server.poolId)
async function handleConnect() {
try {
await connect()
} catch (error) {
console.error('Error when connecting server:', error)
}
}
return {
pool: r => r(poolInfo.value),
hostIp: r => r(server.masterHostIp),
@@ -120,7 +147,34 @@ const { HeadCells, BodyCells } = useServerColumns({
}
: undefined,
}),
selectItem: r => r(() => (selectedServerId.value = server.id)),
actions: r =>
r({
onClick: () => (selectedServerId.value = server.id),
actions: [
server.status === 'connected'
? {
label: t('action:disconnect-pool'),
icon: 'action:disconnect',
busy: isDisconnecting.value,
disabled: !canDisconnect.value,
onClick: () => openDisconnectModal(),
}
: {
label: t('action:connect-pool'),
icon: 'action:connect',
busy: isConnecting.value,
disabled: !canConnect.value,
onClick: () => handleConnect(),
},
{
label: t('action:download-bugtools-archive'),
icon: 'action:download',
busy: isDownloadBusy.value,
disabled: isDownloadDisabled.value,
onClick: () => downloadBugTools(),
},
],
}),
}
},
})

View File

@@ -1,5 +1,20 @@
<template>
<VtsSidePanel :has-selection="!!server" @close="emit('close')">
<template v-if="server" #actions>
<PoolConnectionToggleButton :server-id="server.id" />
<MenuList v-if="server.poolId !== undefined" placement="bottom-end">
<template #trigger="{ open }">
<UiButtonIcon
v-tooltip="{ placement: 'left', content: t('more-actions') }"
accent="brand"
icon="action:more-actions"
size="medium"
@click="open($event)"
/>
</template>
<PoolDownloadButton :pool-id="server.poolId" />
</MenuList>
</template>
<template v-if="server">
<VtsStateHero v-if="!arePoolsReady" format="panel" type="busy" size="medium" />
<template v-else>
@@ -160,17 +175,21 @@
<script setup lang="ts">
import { useXoHostCollection } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import PoolConnectionToggleButton from '@/modules/pool/components/actions/connection/PoolConnectionToggleButton.vue'
import PoolDownloadButton from '@/modules/pool/components/actions/download/PoolDownloadButton.vue'
import { useXoPoolCollection } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
import type { FrontXoServer } from '@/modules/server/remote-resources/use-xo-server-collection.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCardObjectTitle from '@core/components/card-object-title/VtsCardObjectTitle.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import MenuList from '@core/components/menu/MenuList.vue'
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import VtsTag from '@core/components/tag/VtsTag.vue'
import UiAlert from '@core/components/ui/alert/UiAlert.vue'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.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'
@@ -178,6 +197,7 @@ import UiInfo from '@core/components/ui/info/UiInfo.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiLogEntryViewer from '@core/components/ui/log-entry-viewer/UiLogEntryViewer.vue'
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useMapper } from '@core/packages/mapper'
import { toLower } from 'lodash-es'
import { computed } from 'vue'

View File

@@ -0,0 +1,30 @@
import { useXoHostCollection } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import type { FrontXoPool } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
import { downloadBugTools } from '@core/utils/download-bugtools.utils.ts'
import { computed, type MaybeRefOrGetter, toValue } from 'vue'
export function usePoolBugToolsDownload(rawPoolId: MaybeRefOrGetter<FrontXoPool['id'] | undefined>) {
const { getMasterHostByPoolId, areHostsFetching, areHostsReady, hasHostFetchError } = useXoHostCollection()
const masterHost = computed(() => {
const poolId = toValue(rawPoolId)
return poolId === undefined ? undefined : getMasterHostByPoolId(poolId)
})
const isDisabled = computed(
() => hasHostFetchError.value || (areHostsReady.value && masterHost.value?.address === undefined)
)
function download() {
const address = masterHost.value?.address
if (address === undefined) {
return
}
downloadBugTools(address)
}
return { download, isBusy: areHostsFetching, isDisabled }
}

View File

@@ -1,5 +1,5 @@
<template>
<VtsModal accent="info" icon="status:info-picto">
<VtsModal accent="warning" icon="status:warning-picto" dismissible>
<template #title>
<I18nT keypath="confirm-disconnect" scope="global" tag="div">
<span class="n-disconnect">{{ title }}</span>
@@ -7,7 +7,7 @@
</template>
<template #content>
{{ t('pool-disconnect-info') }}
{{ t('modal:pool-disconnect-message') }}
</template>
<template #buttons>
@@ -23,7 +23,7 @@
import VtsModal from '@core/components/modal/VtsModal.vue'
import VtsModalCancelButton from '@core/components/modal/VtsModalCancelButton.vue'
import VtsModalConfirmButton from '@core/components/modal/VtsModalConfirmButton.vue'
import { useI18n } from 'vue-i18n'
import { I18nT, useI18n } from 'vue-i18n'
defineProps<{
title: string
@@ -34,6 +34,6 @@ const { t } = useI18n()
<style lang="postcss" scoped>
.n-disconnect {
color: var(--color-info-item-base);
color: var(--color-warning-item-base);
}
</style>

View File

@@ -0,0 +1,34 @@
import { useXoServerDisconnectJob } from '@/modules/server/jobs/xo-server-disconnect.job.ts'
import {
useXoServerCollection,
type FrontXoServer,
} from '@/modules/server/remote-resources/use-xo-server-collection.ts'
import { useModal } from '@core/packages/modal/use-modal.ts'
import { toComputed } from '@core/utils/to-computed.util.ts'
import { computed, type MaybeRefOrGetter } from 'vue'
export function useServerDisconnectModal(rawServerId: MaybeRefOrGetter<FrontXoServer['id']>) {
const serverId = toComputed(rawServerId)
const { getServerById } = useXoServerCollection()
const server = computed(() => getServerById(serverId.value))
const serverLabel = computed(() => server.value?.poolNameLabel ?? server.value?.label ?? '')
const { run, canRun, isRunning, errorMessage } = useXoServerDisconnectJob([serverId])
const openModal = useModal(() => ({
component: import('@/modules/server/components/modal/ServerDisconnectModal.vue'),
props: { title: serverLabel.value },
onConfirm: async () => {
try {
await run()
} catch (error) {
console.error('Error when disconnecting server:', error)
}
},
}))
return { openModal, canRun, isRunning, errorMessage }
}

View File

@@ -3,24 +3,35 @@ import { useXoTaskUtils } from '@/shared/composables/xo-task-utils.composable.ts
import { fetchPost } from '@/shared/utils/fetch.util.ts'
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
import type { XoTask } from '@vates/types'
import { useI18n } from 'vue-i18n'
export const useXoServerConnectJob = defineJob('server.connect', [xoServerIdsArg], () => {
const { t } = useI18n()
const { monitorTask } = useXoTaskUtils()
return {
run(serverIds) {
return Promise.allSettled(
async run(serverIds) {
const results = await Promise.allSettled(
serverIds.map(async serverId => {
const { taskId } = await fetchPost<{ taskId: XoTask['id'] }>(`servers/${serverId.value}/actions/connect`)
await monitorTask(taskId)
})
)
results.forEach((result, index) => {
if (result.status === 'rejected') {
console.error(`Failed to connect server ${serverIds[index].value}:`, result.reason)
}
})
return results
},
validate(isRunning, serverIds) {
if (isRunning) {
throw new JobRunningError('connect server already running')
throw new JobRunningError(t('job:connect:in-progress'))
}
if (serverIds.length === 0 || serverIds.some(id => id.value === '')) {
throw new JobError('server ids are required')
throw new JobError(t('job:server-connect:missing-server'))
}
},
}

View File

@@ -4,8 +4,10 @@ import { useXoTaskUtils } from '@/shared/composables/xo-task-utils.composable.ts
import { fetchPost } from '@/shared/utils/fetch.util.ts'
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
import type { XoTask } from '@vates/types'
import { useI18n } from 'vue-i18n'
export const useXoServerDisconnectJob = defineJob('server.disconnect', [xoServerIdsArg], () => {
const { t } = useI18n()
const { monitorTask } = useXoTaskUtils()
const { getServersByIds } = useXoServerCollection()
@@ -17,23 +19,25 @@ export const useXoServerDisconnectJob = defineJob('server.disconnect', [xoServer
await monitorTask(taskId)
})
)
results.forEach((result, index) => {
if (result.status === 'rejected') {
console.error(`Failed to disconnect server ${serverIds[index].value}:`, result.reason)
}
})
return results
},
validate(isRunning, serverIds) {
if (isRunning) {
throw new JobRunningError('server disconnection already in progress')
throw new JobRunningError(t('job:disconnect:in-progress'))
}
if (serverIds.length === 0 || serverIds.some(id => id.value === '')) {
throw new JobError('server ids are required')
throw new JobError(t('job:server-disconnect:missing-server'))
}
const servers = getServersByIds(serverIds.map(id => id.value))
if (servers.some(server => server.status !== 'connected')) {
throw new JobError('server must be connected')
throw new JobError(t('job:server-disconnect:server-disconnected'))
}
},
}

View File

@@ -52,6 +52,7 @@
- [XO6] Input fields now support prefix/suffix sections, and display validation messages ordered by severity (PR [#10009](https://github.com/vatesfr/xen-orchestra/pull/10009))
- [XO6/New VM] Add the possibility to set the HA restart priority when creating a VM (PR [#10072](https://github.com/vatesfr/xen-orchestra/pull/10072))
- [REST API] Add `POST /srs` REST route to create a storage repository (PR [#9990](https://github.com/vatesfr/xen-orchestra/pull/9990))
- [XO6/Pool] Add connect/disconnect action on page Site (PR [#10075])(https://github.com/vatesfr/xen-orchestra/pull/10075)
### Bug fixes