feat(lite): add host side panel in pool page (#10580)

This commit is contained in:
Joris K
2026-10-27 14:21:44 +01:00
committed by GitHub
parent a846ae68dd
commit c234800468
32 changed files with 798 additions and 198 deletions

View File

@@ -17,6 +17,7 @@
- [New/VM] Fix `MAP_DUPLICATE_KEY` error that could randomly prevent the VM creation (PR [#10552](https://github.com/vatesfr/xen-orchestra/pull/10552))
- [Host/Network] Add the possibility to forget a PIF (PR [#10239](https://github.com/vatesfr/xen-orchestra/pull/10239))
- [Lite] Display dates and times as `YYYY-MM-DD HH:mm:ss` (PR [#10547](https://github.com/vatesfr/xen-orchestra/pull/10547))
- [Pool/Hosts] Add a side panel displaying the selected host's details, and a search bar to filter the hosts table (PR [#10580](https://github.com/vatesfr/xen-orchestra/pull/10580))
## **0.26.0** (2026-10-01)

View File

@@ -30,16 +30,10 @@
<VtsKeyValueRow :label="t('uuid')" :value="host.uuid" copy />
<VtsKeyValueRow :label="t('description')" :value="host.name_description" />
<VtsKeyValueRow :label="t('version')" :value="host.software_version.product_version" />
<VtsKeyValueRow
:label="t('hardware')"
:value="`${host.bios_strings['system-manufacturer']} (${host.bios_strings['system-product-name']})`"
/>
<VtsKeyValueRow :label="t('hardware')" :value="manufacturerInfo" />
</VtsKeyValueList>
<VtsKeyValueList>
<VtsKeyValueRow
:label="t('cores-with-sockets')"
:value="`${host.cpu_info.cpu_count} (${host.cpu_info.socket_count})`"
/>
<VtsKeyValueRow :label="t('cores-with-sockets')" :value="coreSocketInfo" />
<VtsKeyValueRow :label="t('ram')" :value="`${ram?.value} ${ram?.prefix}`" />
<VtsKeyValueRow :label="t('tags')">
<template #value>
@@ -54,10 +48,11 @@
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { useHostUtils } from '@/modules/host/composables/host-utils.composable.ts'
import { getHostCoreSocketInfo, getHostManufacturerInfo } from '@/modules/host/utils/host.util.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import type { IconName } from '@core/icons'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsKeyValueList from '@core/components/key-value-list/VtsKeyValueList.vue'
import VtsKeyValueRow from '@core/components/key-value-row/VtsKeyValueRow.vue'
@@ -65,10 +60,8 @@ import VtsQuickInfoCard from '@core/components/quick-info-card/VtsQuickInfoCard.
import VtsTag from '@core/components/tag/VtsTag.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
import useRelativeTime from '@core/composables/relative-time.composable.ts'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { formatSizeRaw } from '@core/utils/size.util.ts'
import { parseDateTime } from '@core/utils/time.util.ts'
import { logicAnd } from '@vueuse/math'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -79,24 +72,17 @@ const { host } = defineProps<{
const { t } = useI18n()
const { isMasterHost, masterHost, isReady: isPoolReady } = usePoolStore().subscribe()
const { masterHost, isReady: isPoolReady } = usePoolStore().subscribe()
const { isReady: isHostReady } = useHostStore().subscribe()
const { isHostRunning, getHostMemory, isReady: isHostMetricsReady } = useHostMetricsStore().subscribe()
const { getHostMemory, isReady: isHostMetricsReady } = useHostMetricsStore().subscribe()
const { isMaster, isRunning, powerState, relativeStartTime } = useHostUtils(() => host)
const isReady = logicAnd(isPoolReady, isHostReady, isHostMetricsReady)
const isRunning = computed(() => isHostRunning(host))
const manufacturerInfo = computed(() => getHostManufacturerInfo(host))
const powerState = computed<{ text: string; icon: IconName }>(() => ({
text: isRunning.value ? t(`host:status:running`) : t('host:status:halted'),
icon: isRunning.value ? 'status:running-circle' : 'status:halted-circle',
}))
const date = computed(() => new Date(parseDateTime(Number(host.other_config.boot_time) * 1000)))
const relativeStartTime = useRelativeTime(date)
const isMaster = computed(() => isMasterHost(host.$ref))
const coreSocketInfo = computed(() => getHostCoreSocketInfo(host))
const ram = computed(() => {
const memory = getHostMemory(host)

View File

@@ -58,7 +58,7 @@
<script setup lang="ts">
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { useHostUtils } from '@/modules/host/composables/host-utils.composable.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsRelativeTime from '@core/components/relative-time/VtsRelativeTime.vue'
@@ -71,7 +71,6 @@ import UiLink from '@core/components/ui/link/UiLink.vue'
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { host } = defineProps<{
@@ -80,9 +79,7 @@ const { host } = defineProps<{
const { t } = useI18n()
const { pool, isMasterHost, masterHost } = usePoolStore().subscribe()
const { isHostRunning } = useHostMetricsStore().subscribe()
const { pool, masterHost } = usePoolStore().subscribe()
const isMaster = computed(() => isMasterHost(host.$ref))
const isRunning = computed(() => isHostRunning(host))
const { isMaster, isRunning } = useHostUtils(() => host)
</script>

View File

@@ -4,19 +4,13 @@
{{ t('hardware-specifications') }}
</UiTitle>
<VtsTabularKeyValueList>
<VtsTabularKeyValueRow
:label="t('manufacturer-info')"
:value="`${host.bios_strings['system-manufacturer']} (${host.bios_strings['system-product-name']})`"
/>
<VtsTabularKeyValueRow :label="t('manufacturer-info')" :value="manufacturerInfo" />
<VtsTabularKeyValueRow
:label="t('bios-info')"
:value="`${host.bios_strings['bios-vendor']} (${host.bios_strings['bios-version']})`"
/>
<VtsTabularKeyValueRow :label="t('cpu-model')" :value="host.cpu_info.modelname" />
<VtsTabularKeyValueRow
:label="t('core-socket')"
:value="`${host.cpu_info.cpu_count} (${host.cpu_info.socket_count})`"
/>
<VtsTabularKeyValueRow :label="t('core-socket')" :value="coreSocketInfo" />
<VtsTabularKeyValueRow :label="t('gpus')">
<template v-if="isReady" #value>
<template v-if="devicesNames">
@@ -33,6 +27,7 @@
<script setup lang="ts">
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { getHostCoreSocketInfo, getHostManufacturerInfo } from '@/modules/host/utils/host.util.ts'
import { usePciStore } from '@/stores/xen-api/pci.store.ts'
import { usePgpuStore } from '@/stores/xen-api/pgpu.store.ts'
import VtsTabularKeyValueList from '@core/components/tabular-key-value-list/VtsTabularKeyValueList.vue'
@@ -41,6 +36,7 @@ import UiCard from '@core/components/ui/card/UiCard.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { logicAnd } from '@vueuse/math'
import { useArrayReduce } from '@vueuse/shared'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { host } = defineProps<{
@@ -54,6 +50,10 @@ const { getByOpaqueRef: getPciByOpaqueRef, isReady: isPciReady } = usePciStore()
const isReady = logicAnd(isPgpuReady, isPciReady)
const manufacturerInfo = computed(() => getHostManufacturerInfo(host))
const coreSocketInfo = computed(() => getHostCoreSocketInfo(host))
const devicesNames = useArrayReduce(
() => pGpusByHost.value.get(host.$ref) ?? [],
(acc, pGpu) => {

View File

@@ -17,13 +17,12 @@
<script setup lang="ts">
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { useHostUtils } from '@/modules/host/composables/host-utils.composable.ts'
import VtsRelativeTime from '@core/components/relative-time/VtsRelativeTime.vue'
import VtsTabularKeyValueList from '@core/components/tabular-key-value-list/VtsTabularKeyValueList.vue'
import VtsTabularKeyValueRow from '@core/components/tabular-key-value-row/VtsTabularKeyValueRow.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { host } = defineProps<{
@@ -32,7 +31,5 @@ const { host } = defineProps<{
const { t } = useI18n()
const { isHostRunning } = useHostMetricsStore().subscribe()
const isRunning = computed(() => isHostRunning(host))
const { isRunning } = useHostUtils(() => host)
</script>

View File

@@ -23,6 +23,7 @@
<script setup lang="ts">
import type { XenApiPif } from '@/libs/xen-api/xen-api.types.ts'
import { getHostState } from '@/modules/host/utils/host.util.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { usePifStore } from '@/stores/xen-api/pif.store.ts'
@@ -41,7 +42,7 @@ const { pif } = defineProps<{
const { t } = useI18n()
const { getByOpaqueRef } = useHostStore().subscribe()
const { isHostRunning } = useHostMetricsStore().subscribe()
const { getHostPowerState } = useHostMetricsStore().subscribe()
const { getPifStatus } = usePifStore().subscribe()
const router = useRouter()
@@ -57,7 +58,7 @@ const pifHost = computed(() => {
return {
label: host.name_label,
powerState: isHostRunning(host) ? 'running' : 'halted',
powerState: getHostState(host, getHostPowerState(host)),
redirect() {
router.push({
name: '/host/[uuid]/network',

View File

@@ -21,7 +21,7 @@
v-if="host"
:to="`/host/${host.uuid}`"
size="medium"
:icon="`object:host:${hostPowerState}`"
:icon="hostIcon"
:is-primary="isPoolMaster"
:primary-tooltip="t('master')"
>
@@ -56,6 +56,7 @@
<script lang="ts" setup>
import { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums.ts'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
import { getHostIcon } from '@/modules/host/utils/host.util.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import { useVmGuestMetricsStore } from '@/stores/xen-api/vm-guest-metrics.store.ts'
@@ -84,7 +85,7 @@ const { t } = useI18n()
const { isReady, getVmHost } = useVmStore().subscribe()
const { getByOpaqueRef: getGuestMetricsByOpaqueRef } = useVmGuestMetricsStore().subscribe()
const { getByOpaqueRef: getMetricsByOpaqueRef } = useVmMetricsStore().subscribe()
const { isHostRunning } = useHostMetricsStore().subscribe()
const { getHostPowerState } = useHostMetricsStore().subscribe()
const { pool } = usePoolStore().subscribe()
const guestMetrics = computed(() => getGuestMetricsByOpaqueRef(vm.guest_metrics))
@@ -95,13 +96,7 @@ const host = computed(() => getVmHost(vm))
const isPoolMaster = computed(() => pool.value?.master === host.value?.$ref)
const hostPowerState = computed(() => {
if (host.value === undefined) {
return 'unknown'
}
return isHostRunning(host.value) ? 'running' : 'halted'
})
const hostIcon = computed(() => getHostIcon(host.value, getHostPowerState(host.value)))
const powerState = useMapper(
() => vm.power_state,

View File

@@ -1,7 +1,7 @@
import { fetchMissingHostPatches } from '@/libs/host.ts'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import type { XenApiPatch } from '@/types/xen-api.ts'
import { type Pausable, useTimeoutPoll, watchArray } from '@vueuse/core'
import { type Pausable, tryOnScopeDispose, useTimeoutPoll, watchArray } from '@vueuse/core'
import { computed, type MaybeRefOrGetter, reactive, toValue } from 'vue'
export type XenApiPatchWithHostRefs = XenApiPatch & { $hostRefs: Set<string> }
@@ -56,6 +56,8 @@ export const useHostPatches = (hosts: MaybeRefOrGetter<XenApiHost[]>) => {
{ immediate: true }
)
tryOnScopeDispose(() => configByHost.forEach(config => config.timeoutPoll.pause()))
const patches = computed(() => {
const records = new Map<string, XenApiPatchWithHostRefs>()

View File

@@ -0,0 +1,145 @@
<template>
<div class="hosts-table">
<UiTitle>
{{ t('hosts') }}
</UiTitle>
<UiQuerySearchBar @search="(value: string) => (searchQuery = value)" />
<div class="container">
<VtsTable :pagination-bindings :state sticky="right">
<thead>
<tr>
<HeadCells />
</tr>
</thead>
<tbody>
<VtsRow v-for="host of paginatedHosts" :key="host.$ref" :selected="selectedHostId === host.uuid">
<BodyCells :item="host" />
</VtsRow>
</tbody>
</VtsTable>
</div>
</div>
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { getHostIcon } from '@/modules/host/utils/host.util.ts'
import { getHostIpAddresses } from '@/modules/pif/utils/pif.util.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { usePifStore } from '@/stores/xen-api/pif.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.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 { icon } from '@core/icons'
import { useHostColumns } from '@core/tables/column-sets/host-columns.ts'
import { logicAnd, logicNot, logicOr } from '@vueuse/math'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const {
hosts: rawHosts,
busy,
error,
} = defineProps<{
hosts: XenApiHost[]
busy?: boolean
error?: boolean
}>()
const { t } = useI18n()
const { getHostPowerState } = useHostMetricsStore().subscribe()
const { isMasterHost } = usePoolStore().subscribe()
const { pifsByHost, isReady: arePifsReady, hasError: hasPifError } = usePifStore().subscribe()
const isReady = logicAnd(() => !busy, arePifsReady)
const hasError = logicOr(() => error, hasPifError)
const selectedHostId = useRouteQuery('id')
const searchQuery = ref('')
const filteredHosts = computed(() => {
const searchTerm = searchQuery.value.trim().toLocaleLowerCase()
if (!searchTerm) {
return rawHosts
}
return rawHosts.filter(host =>
Object.values(host).some(value => String(value).toLocaleLowerCase().includes(searchTerm))
)
})
const state = useTableState({
busy: logicNot(isReady),
error: hasError,
empty: () => {
if (rawHosts.length === 0) {
return t('no-host-detected')
}
if (filteredHosts.value.length === 0) {
return { type: 'no-result' }
}
return false
},
})
const { pageRecords: paginatedHosts, paginationBindings } = usePagination('hosts', filteredHosts)
function getMasterIcon(host: XenApiHost) {
if (!isMasterHost(host.$ref)) {
return undefined
}
return {
icon: icon('status:primary-circle'),
tooltip: t('master'),
}
}
const { HeadCells, BodyCells } = useHostColumns({
exclude: ['checkbox', 'srStatus'],
body: (host: XenApiHost) => {
const ipAddresses = computed(() => getHostIpAddresses(host.address, pifsByHost.value.get(host.$ref)))
const hostIcon = computed(() => getHostIcon(host, getHostPowerState(host)))
const rightIcon = computed(() => getMasterIcon(host))
return {
host: r =>
r({
label: host.name_label,
to: { name: '/host/[uuid]/dashboard', params: { uuid: host.uuid } },
icon: hostIcon.value,
rightIcon: rightIcon.value,
}),
description: r => r(host.name_description),
ipAddresses: r => r(ipAddresses.value),
tags: r => r(host.tags),
selectItem: r => r(() => (selectedHostId.value = host.uuid)),
}
},
})
</script>
<style lang="postcss" scoped>
.hosts-table {
display: flex;
flex-direction: column;
gap: 2.4rem;
.container {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
}
</style>

View File

@@ -0,0 +1,29 @@
<template>
<VtsSidePanel :has-selection="!!host" @close="emit('close')">
<template v-if="host" #default>
<HostInfosCard :host />
<HostSrsCard :host />
<HostNetworkCard :host />
<HostSoftwareCard :host />
<HostHardwareSpecificationsCard :host />
</template>
</VtsSidePanel>
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import HostHardwareSpecificationsCard from '@/modules/host/components/list/panel/cards/HostHardwareSpecificationsCard.vue'
import HostInfosCard from '@/modules/host/components/list/panel/cards/HostInfosCard.vue'
import HostNetworkCard from '@/modules/host/components/list/panel/cards/HostNetworkCard.vue'
import HostSoftwareCard from '@/modules/host/components/list/panel/cards/HostSoftwareCard.vue'
import HostSrsCard from '@/modules/host/components/list/panel/cards/HostSrsCard.vue'
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
defineProps<{
host?: XenApiHost
}>()
const emit = defineEmits<{
close: []
}>()
</script>

View File

@@ -0,0 +1,52 @@
<template>
<UiPanelCard class="host-hardware-specifications-card">
<UiPanelCardTitle size="medium" :label="t('hardware-specifications')" />
<div class="content">
<VtsCardRowKeyValue>
<template #key>{{ t('manufacturer-info') }}</template>
<template #value>{{ manufacturerInfo }}</template>
<template #addons>
<VtsCopyButton :value="manufacturerInfo" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue>
<template #key>{{ t('core-socket') }}</template>
<template #value>{{ coreSocketInfo }}</template>
<template #addons>
<VtsCopyButton :value="coreSocketInfo" />
</template>
</VtsCardRowKeyValue>
</div>
</UiPanelCard>
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { getHostCoreSocketInfo, getHostManufacturerInfo } from '@/modules/host/utils/host.util.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { host } = defineProps<{
host: XenApiHost
}>()
const { t } = useI18n()
const manufacturerInfo = computed(() => getHostManufacturerInfo(host))
const coreSocketInfo = computed(() => getHostCoreSocketInfo(host))
</script>
<style lang="postcss" scoped>
.host-hardware-specifications-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -0,0 +1,160 @@
<template>
<UiPanelCard class="host-infos-card">
<UiPanelCardTitle
:id="host.uuid"
size="medium"
:label="host.name_label"
:icon="hostIcon"
:to="{ name: '/host/[uuid]/dashboard', params: { uuid: host.uuid } }"
/>
<div class="content">
<VtsCardRowKeyValue>
<template #key>{{ t('state') }}</template>
<template #value>
<span class="value">
<VtsIcon :name="powerState.icon" size="medium" />
{{ powerState.text }}
</span>
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue truncate align-top>
<template #key>{{ t('description') }}</template>
<template #value>{{ host.name_description }}</template>
<template v-if="host.name_description" #addons>
<VtsCopyButton :value="host.name_description" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue align-top>
<template #key>{{ t('tags') }}</template>
<template #value>
<UiTagsList v-if="host.tags.length > 0">
<VtsTag v-for="tag in host.tags" :key="tag" :value="tag" />
</UiTagsList>
</template>
<template v-if="host.tags.length > 0" #addons>
<VtsCopyButton :value="host.tags.join(', ')" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue>
<template #key>{{ t('status') }}</template>
<template #value>
<VtsEnabledState :enabled="host.enabled" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue>
<template #key>{{ t('pool') }}</template>
<template #value>
<div v-if="pool !== undefined" class="value">
<UiLink
:to="{ name: '/pool/[uuid]/dashboard', params: { uuid: pool.uuid } }"
size="small"
icon="object:pool"
>
{{ pool.name_label }}
</UiLink>
</div>
</template>
<template v-if="pool !== undefined" #addons>
<VtsCopyButton :value="pool.uuid" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue>
<template #key>{{ t('master') }}</template>
<template #value>
<div v-if="isMaster" class="value">
<VtsIcon v-tooltip="t('master')" name="status:primary-circle" size="medium" />
{{ t('this-host') }}
</div>
<div v-else-if="masterHost !== undefined" class="value">
<UiLink
:to="{ name: '/host/[uuid]/dashboard', params: { uuid: masterHost.uuid } }"
size="small"
:icon="masterHostIcon"
is-primary
:primary-tooltip="t('master')"
>
{{ masterHost.name_label }}
</UiLink>
</div>
</template>
<template v-if="masterHost !== undefined" #addons>
<VtsCopyButton :value="masterHost.uuid" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue>
<template #key>{{ t('started') }}</template>
<template #value>{{ relativeStartTime }}</template>
<template v-if="relativeStartTime" #addons>
<VtsCopyButton :value="relativeStartTime" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue>
<template #key>{{ t('patches') }}</template>
<template v-if="areAllLoaded" #value>
<UiInfo v-if="noMissingPatches" accent="success">{{ t('patches-up-to-date') }}</UiInfo>
<UiInfo v-else accent="warning">{{ t('n-missing', nMissingPatches) }}</UiInfo>
</template>
<template v-if="areAllLoaded" #addons>
<VtsCopyButton :value="noMissingPatches ? t('patches-up-to-date') : t('n-missing', nMissingPatches)" />
</template>
</VtsCardRowKeyValue>
</div>
</UiPanelCard>
</template>
<script lang="ts" setup>
import { useHostPatches } from '@/composables/host-patches.composable.ts'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { useHostUtils } from '@/modules/host/composables/host-utils.composable.ts'
import { getHostIcon } from '@/modules/host/utils/host.util.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import VtsEnabledState from '@core/components/enabled-state/VtsEnabledState.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsTag from '@core/components/tag/VtsTag.vue'
import UiInfo from '@core/components/ui/info/UiInfo.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { host } = defineProps<{
host: XenApiHost
}>()
const { t } = useI18n()
const { pool, masterHost } = usePoolStore().subscribe()
const { getHostPowerState } = useHostMetricsStore().subscribe()
const { isMaster, powerState, relativeStartTime } = useHostUtils(() => host)
const hostIcon = computed(() => getHostIcon(host, getHostPowerState(host)))
const masterHostIcon = computed(() => getHostIcon(masterHost.value, getHostPowerState(masterHost.value)))
const { count: nMissingPatches, areAllLoaded } = useHostPatches(() => [host])
const noMissingPatches = computed(() => nMissingPatches.value === 0)
</script>
<style lang="postcss" scoped>
.host-infos-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
.value {
display: flex;
align-items: center;
gap: 0.8rem;
}
}
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<UiPanelCard class="host-network-card">
<UiPanelCardTitle :label="t('network')" size="medium" />
<div class="content">
<VtsCardRowKeyValue>
<template #key>{{ t('management-ip') }}</template>
<template #value>{{ host.address }}</template>
<template v-if="host.address" #addons>
<VtsCopyButton :value="host.address" />
</template>
</VtsCardRowKeyValue>
<template v-if="ipV4Addresses.length > 0">
<VtsCardRowKeyValue v-for="(ip, index) in ipV4Addresses" :key="ip">
<template #key>
<div v-if="index === 0">{{ t('ipv4-addresses') }}</div>
</template>
<template #value>{{ ip }}</template>
<template #addons>
<VtsCopyButton :value="ip" />
<VtsCopyAllMenuItem v-if="index === 0 && ipV4Addresses.length > 1" :values="ipV4Addresses" />
</template>
</VtsCardRowKeyValue>
</template>
<VtsCardRowKeyValue v-else>
<template #key>{{ t('ipv4-addresses') }}</template>
</VtsCardRowKeyValue>
<template v-if="ipV6Addresses.length > 0">
<VtsCardRowKeyValue v-for="(ip, index) in ipV6Addresses" :key="ip">
<template #key>
<div v-if="index === 0">{{ t('ipv6-addresses') }}</div>
</template>
<template #value>{{ ip }}</template>
<template #addons>
<VtsCopyButton :value="ip" />
<VtsCopyAllMenuItem v-if="index === 0 && ipV6Addresses.length > 1" :values="ipV6Addresses" />
</template>
</VtsCardRowKeyValue>
</template>
<VtsCardRowKeyValue v-else>
<template #key>{{ t('ipv6-addresses') }}</template>
</VtsCardRowKeyValue>
</div>
</UiPanelCard>
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { usePifStore } from '@/stores/xen-api/pif.store.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { host } = defineProps<{
host: XenApiHost
}>()
const { t } = useI18n()
const { pifsByHost } = usePifStore().subscribe()
const pifs = computed(() => pifsByHost.value.get(host.$ref))
const ipV4Addresses = computed(() => pifs.value?.map(pif => pif.IP).filter(ip => ip) ?? [])
const ipV6Addresses = computed(() => pifs.value?.flatMap(pif => pif.IPv6.filter(ip => ip)) ?? [])
</script>
<style lang="postcss" scoped>
.host-network-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<UiPanelCard class="host-software-card">
<UiPanelCardTitle size="medium" :label="t('software')" />
<div class="content">
<VtsCardRowKeyValue>
<template #key>{{ t('version') }}</template>
<template #value>{{ host.software_version.product_version }}</template>
<template #addons>
<VtsCopyButton :value="host.software_version.product_version" />
</template>
</VtsCardRowKeyValue>
</div>
</UiPanelCard>
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
import { useI18n } from 'vue-i18n'
defineProps<{
host: XenApiHost
}>()
const { t } = useI18n()
</script>
<style lang="postcss" scoped>
.host-software-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('connected-srs')" :counter="srs.length" />
<VtsStateHero v-if="!isReady" format="card" type="busy" size="extra-small" />
<VtsStateHero v-else-if="hasError" format="card" type="error" horizontal size="extra-small">
{{ t('error-no-data') }}
</VtsStateHero>
<UiCollapsibleList v-else-if="srs.length > 0" tag="ul" :total-items="srs.length">
<li v-for="sr in srs" :key="sr.uuid" v-tooltip class="text-ellipsis">
<UiLink
size="small"
:icon="getSrStatusIcon(sr)"
:to="{ name: '/host/[uuid]/storage', params: { uuid: host.uuid }, query: { id: sr.uuid } }"
>
{{ sr.name_label }}
</UiLink>
</li>
</UiCollapsibleList>
<VtsStateHero v-else type="no-data" format="card" horizontal size="extra-small">
{{ t('no-sr-connected') }}
</VtsStateHero>
</UiPanelCard>
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { useGetPbdsInScope, useSrUtils } from '@/modules/storage-repository/composables/sr-utils.composable.ts'
import { usePbdStore } from '@/stores/xen-api/pbd.store.ts'
import { useSrStore } from '@/stores/xen-api/sr.store.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCollapsibleList from '@core/components/ui/collapsible-list/UiCollapsibleList.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import { logicAnd, logicOr } from '@vueuse/math'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { host } = defineProps<{
host: XenApiHost
}>()
const { t } = useI18n()
const { getSrsForHost, isReady: areSrsReady, hasError: hasSrError } = useSrStore().subscribe()
const { isReady: arePbdsReady, hasError: hasPbdError } = usePbdStore().subscribe()
const isReady = logicAnd(areSrsReady, arePbdsReady)
const hasError = logicOr(hasSrError, hasPbdError)
const scope = computed<SrScope>(() => ({ type: SR_SCOPE_TYPE.HOST, hostId: host.$ref }))
const { isConnectedInScope } = useGetPbdsInScope()
const { getSrStatusIcon } = useSrUtils(undefined, scope)
const srs = computed(() => getSrsForHost(host.$ref).filter(sr => isConnectedInScope(sr, scope.value)))
</script>

View File

@@ -2,8 +2,13 @@ import { HOST_OPERATION } from '@/libs/xen-api/xen-api.enums.ts'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { getHostPendingStateOperation, getHostState, type HostState } from '@/modules/host/utils/host.util.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import type { IconName } from '@core/icons'
import { useTimeAgo } from '@core/composables/locale-time-ago.composable.ts'
import { useMapper } from '@core/packages/mapper'
import { parseDateTime } from '@core/utils/time.util.ts'
import { toComputed } from '@core/utils/to-computed.util.ts'
import { HOST_POWER_STATE } from '@vates/types'
import { computed, type MaybeRefOrGetter } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -12,22 +17,53 @@ export function useHostUtils(rawHost: MaybeRefOrGetter<XenApiHost | undefined>)
const host = toComputed(rawHost)
const { isHostRunning, getHostPowerState } = useHostMetricsStore().subscribe()
const { getHostPowerState } = useHostMetricsStore().subscribe()
const { isMasterHost } = usePoolStore().subscribe()
const isMaster = computed(() => {
if (host.value === undefined) {
return false
}
return isMasterHost(host.value.$ref)
})
const hostPowerState = computed(() => getHostPowerState(host.value))
const isRunning = computed(() => hostPowerState.value === HOST_POWER_STATE.RUNNING)
const hostState = computed(() => getHostState(host.value, hostPowerState.value))
const powerState = useMapper<HostState, { text: string; icon: IconName | undefined }>(
hostState,
() => ({
running: { text: t('host:status:running'), icon: 'status:running-circle' },
halted: { text: t('host:status:halted'), icon: 'status:halted-circle' },
disabled: { text: t('host:status:disabled'), icon: 'status:host-disabled-circle' },
unknown: { text: t('host:status:unknown'), icon: undefined },
}),
'unknown'
)
const bootTime = computed(() => Number(host.value?.other_config.boot_time))
const hostTimeAgo = useTimeAgo(() => new Date(parseDateTime((bootTime.value || 0) * 1000)))
const relativeStartTime = computed(() => {
if (!bootTime.value) {
return undefined
}
return hostTimeAgo.value
})
const pendingStateOperation = computed(() => {
if (host.value === undefined) {
return undefined
}
return getHostPendingStateOperation(host.value, isHostRunning(host.value))
})
const hostState = computed<HostState>(() => {
if (host.value === undefined) {
return 'unknown'
}
return getHostState(host.value, getHostPowerState(host.value))
return getHostPendingStateOperation(host.value, isRunning.value)
})
const isChangingState = computed(() => pendingStateOperation.value !== undefined)
@@ -46,8 +82,12 @@ export function useHostUtils(rawHost: MaybeRefOrGetter<XenApiHost | undefined>)
)
return {
isMaster,
isRunning,
hostState,
powerState,
isChangingState,
currentOperation,
relativeStartTime,
}
}

View File

@@ -1,5 +1,6 @@
import { HOST_OPERATION } from '@/libs/xen-api/xen-api.enums.ts'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { objectIcon } from '@core/icons'
import { HOST_POWER_STATE } from '@vates/types'
import { castArray } from 'lodash-es'
@@ -32,8 +33,8 @@ export const getHostPendingStateOperation = (host: XenApiHost, isHostRunning: bo
isHostRunning ? RUNNING_CHANGING_STATE_OPERATIONS : NOT_RUNNING_CHANGING_STATE_OPERATIONS
)
export const getHostState = (host: XenApiHost, powerState: HOST_POWER_STATE): HostState => {
if (powerState === HOST_POWER_STATE.UNKNOWN) {
export const getHostState = (host: XenApiHost | undefined, powerState: HOST_POWER_STATE): HostState => {
if (host === undefined || powerState === HOST_POWER_STATE.UNKNOWN) {
return 'unknown'
}
@@ -43,3 +44,16 @@ export const getHostState = (host: XenApiHost, powerState: HOST_POWER_STATE): Ho
return host.enabled ? 'running' : 'disabled'
}
export const getHostIcon = (host: XenApiHost | undefined, powerState: HOST_POWER_STATE) =>
objectIcon('host', getHostState(host, powerState))
export const getHostCoreSocketInfo = (host: XenApiHost): string =>
`${host.cpu_info.cpu_count} (${host.cpu_info.socket_count})`
export const getHostManufacturerInfo = (host: XenApiHost): string => {
const manufacturer = host.bios_strings['system-manufacturer'] ?? ''
const productName = host.bios_strings['system-product-name']
return manufacturer + (productName ? ` (${productName})` : '')
}

View File

@@ -0,0 +1,7 @@
import type { XenApiPif } from '@/libs/xen-api/xen-api.types.ts'
export function getHostIpAddresses(managementIp: string, pifs: XenApiPif[] = []): string[] {
const others = pifs.flatMap(pif => [pif.IP, ...pif.IPv6]).filter(ip => ip && ip !== managementIp)
return managementIp ? [managementIp, ...others] : others
}

View File

@@ -5,7 +5,7 @@
<li v-for="host in hosts" :key="host.uuid" v-tooltip class="text-ellipsis">
<UiLink
size="small"
:icon="`object:host:${getHostPowerState(host)}`"
:icon="getHostIcon(host, getHostPowerState(host))"
:to="{ name: '/host/[uuid]/dashboard', params: { uuid: host.uuid } }"
>
{{ host.name_label }}
@@ -20,6 +20,7 @@
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { getHostIcon } from '@/modules/host/utils/host.util.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCollapsibleList from '@core/components/ui/collapsible-list/UiCollapsibleList.vue'
@@ -35,9 +36,5 @@ defineProps<{
const { t } = useI18n()
const { isHostRunning } = useHostMetricsStore().subscribe()
function getHostPowerState(host: XenApiHost) {
return isHostRunning(host) ? 'running' : 'halted'
}
const { getHostPowerState } = useHostMetricsStore().subscribe()
</script>

View File

@@ -1,5 +1,6 @@
import type { XenApiPbd, XenApiSr } from '@/libs/xen-api/xen-api.types.ts'
import { usePbdUtils } from '@/modules/storage-repository/composables/pbd-utils.composable.ts'
import { getPbdsConnectionStatus } from '@/modules/storage-repository/utils/pbd.util.ts'
import { usePbdStore } from '@/stores/xen-api/pbd.store.ts'
import { type IconName, objectIcon } from '@core/icons'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
@@ -25,6 +26,10 @@ export function useGetPbdsInScope() {
return getPbdsInScope(sr, scope).filter(pbd => pbd.currently_attached)
}
function isConnectedInScope(sr: XenApiSr, scope: SrScope) {
return getPbdsInScope(sr, scope).some(pbd => pbd.currently_attached)
}
function getDetachedPbdsInScope(sr: XenApiSr, scope: SrScope): XenApiPbd[] {
return getPbdsInScope(sr, scope).filter(pbd => !pbd.currently_attached)
}
@@ -50,6 +55,7 @@ export function useGetPbdsInScope() {
getAttachedPbdsInScope,
getDetachedPbdsInScope,
getSrPbdsSignature,
isConnectedInScope,
isPartiallyConnectedInScope,
}
}
@@ -83,10 +89,15 @@ export function useSrUtils(
const srStatusIcon = computed<IconName>(() => objectIcon('sr', allPbdsConnectionStatus.value))
function getSrStatusIcon(sr: XenApiSr): IconName {
return objectIcon('sr', getPbdsConnectionStatus(getPbdsInScope(sr, scope.value)))
}
return {
pbdsInScope,
srConnectionStatus: allPbdsConnectionStatus,
isPartiallyConnectedInScope,
srStatusIcon,
getSrStatusIcon,
}
}

View File

@@ -0,0 +1,14 @@
import type { XenApiPbd } from '@/libs/xen-api/xen-api.types.ts'
import { CONNECTION_STATUS } from '@core/types/connection.ts'
export const getPbdsConnectionStatus = (pbds: XenApiPbd[]) => {
if (pbds.length === 0 || pbds.every(pbd => !pbd.currently_attached)) {
return CONNECTION_STATUS.DISCONNECTED
}
if (pbds.some(pbd => !pbd.currently_attached)) {
return CONNECTION_STATUS.PARTIALLY_CONNECTED
}
return CONNECTION_STATUS.CONNECTED
}

View File

@@ -40,8 +40,8 @@ usePageTitleStore().setTitle(t('storage'))
const route = useRoute<'/host/[uuid]/storage'>()
const { getByUuid: getHostByUuid } = useHostStore().subscribe()
const { getByOpaqueRef: getSrByOpaqueRef, isReady: areSrsReady, hasError } = useSrStore().subscribe()
const { getPbdsForHost, isReady: arePbdsReady } = usePbdStore().subscribe()
const { getSrsForHost, isReady: areSrsReady, hasError } = useSrStore().subscribe()
const { isReady: arePbdsReady } = usePbdStore().subscribe()
const { pool } = usePoolStore().subscribe()
const uiStore = useUiStore()
@@ -65,19 +65,7 @@ const srs = computed((): XenApiSr[] => {
return []
}
const hostPbds = getPbdsForHost(host.value.$ref)
return hostPbds
.reduce<XenApiSr[]>((acc, pbd) => {
const sr = getSrByOpaqueRef(pbd.SR)
if (sr !== undefined) {
acc.push(sr)
}
return acc
}, [])
.sort(sortByNameLabel)
return getSrsForHost(host.value.$ref).sort(sortByNameLabel)
})
const selectedSr = useRouteQuery<XenApiSr | undefined>('id', {

View File

@@ -1,71 +1,41 @@
<template>
<UiCard class="pool-hosts-view">
<UiTitle>
{{ t('hosts') }}
</UiTitle>
<VtsTable :state :pagination-bindings>
<thead>
<tr>
<HeadCells />
</tr>
</thead>
<tbody>
<VtsRow v-for="host of paginatedHosts" :key="host.$ref">
<BodyCells :item="host" />
</VtsRow>
</tbody>
</VtsTable>
</UiCard>
<VtsContentSidePanel class="pool-hosts-view">
<UiCard class="container">
<HostsTable :hosts :busy="!isReady" :error="hasError" />
</UiCard>
<HostSidePanel :host="selectedHost" @close="selectedHost = undefined" />
</VtsContentSidePanel>
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import HostsTable from '@/modules/host/components/list/HostsTable.vue'
import HostSidePanel from '@/modules/host/components/list/panel/HostSidePanel.vue'
import { usePageTitleStore } from '@/stores/page-title.store.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import VtsContentSidePanel from '@core/components/layout/VtsContentSidePanel.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { usePagination } from '@core/composables/pagination.composable.ts'
import { useTableState } from '@core/composables/table-state.composable.ts'
import { objectIcon } from '@core/icons'
import { useHostColumns } from '@core/tables/column-sets/host-columns.ts'
import { logicNot } from '@vueuse/math'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const { isReady, hasError, records: hosts } = useHostStore().subscribe()
const { isHostRunning } = useHostMetricsStore().subscribe()
usePageTitleStore().setTitle(() => t('hosts'))
const { pageRecords: paginatedHosts, paginationBindings } = usePagination('hosts', hosts)
const state = useTableState({
busy: logicNot(isReady),
error: hasError,
empty: () => (hosts.value.length === 0 ? t('no-host-detected') : false),
})
const { HeadCells, BodyCells } = useHostColumns({
exclude: ['checkbox', 'selectItem', 'srStatus'],
body: (host: XenApiHost) => {
const state = isHostRunning(host) ? 'running' : 'halted'
return {
host: r => r({ label: host.name_label, to: `/host/${host.uuid}/dashboard`, icon: objectIcon('host', state) }),
description: r => r(host.name_description),
tags: r => r(host.tags),
ipAddresses: r => r(host.address),
}
},
const selectedHost = useRouteQuery<XenApiHost | undefined>('id', {
toData: id => hosts.value.find(host => host.uuid === id),
toQuery: host => host?.uuid ?? '',
})
</script>
<style lang="postcss" scoped>
.pool-hosts-view {
margin: 0.8rem;
.container {
height: fit-content;
margin: 0.8rem;
gap: 4rem;
}
}
</style>

View File

@@ -19,8 +19,8 @@ export const useHostMetricsStore = defineStore('xen-api-host-metrics', () => {
}
}
const getHostPowerState = (host: XenApiHost): HOST_POWER_STATE => {
const live = baseContext.getByOpaqueRef(host.metrics)?.live
const getHostPowerState = (host: XenApiHost | undefined): HOST_POWER_STATE => {
const live = host === undefined ? undefined : baseContext.getByOpaqueRef(host.metrics)?.live
if (live === undefined) {
return HOST_POWER_STATE.UNKNOWN

View File

@@ -1,4 +1,4 @@
import type { XenApiNetwork, XenApiPif } from '@/libs/xen-api/xen-api.types.ts'
import type { XenApiHost, XenApiNetwork, XenApiPif } from '@/libs/xen-api/xen-api.types.ts'
import { useBondStore } from '@/stores/xen-api/bond.store.ts'
import { createXapiStoreConfig } from '@/stores/xen-api/create-xapi-store-config.ts'
import { usePifMetricsStore } from '@/stores/xen-api/pif-metrics.store.ts'
@@ -35,6 +35,19 @@ export const usePifStore = defineStore('xen-api-pif', () => {
return hostMasterPifsByNetworkMap
})
const pifsByHost = computed(() => {
const pifsByHostMap = new Map<XenApiHost['$ref'], XenApiPif[]>()
baseContext.records.value.forEach(pif => {
if (!pifsByHostMap.has(pif.host)) {
pifsByHostMap.set(pif.host, [])
}
pifsByHostMap.get(pif.host)?.push(pif)
})
return pifsByHostMap
})
const getPifsByNetworkRef = (networkRef: XenApiNetwork['$ref']) => {
return baseContext.records.value.filter(pif => pif.network === networkRef)
}
@@ -69,6 +82,7 @@ export const usePifStore = defineStore('xen-api-pif', () => {
const context = {
...baseContext,
hostMasterPifsByNetwork,
pifsByHost,
getPifsByNetworkRef,
getPifStatus,
getBondsDevices,

View File

@@ -1,5 +1,5 @@
import { getAllocationStrategy as getAllocationStrategyFromSrType } from '@/libs/sr.ts'
import type { XenApiPool, XenApiSr, XenApiVdi } from '@/libs/xen-api/xen-api.types.ts'
import type { XenApiHost, XenApiPool, XenApiSr, XenApiVdi } from '@/libs/xen-api/xen-api.types.ts'
import { createXapiStoreConfig } from '@/stores/xen-api/create-xapi-store-config.ts'
import { usePbdStore } from '@/stores/xen-api/pbd.store.ts'
import { useVdiStore } from '@/stores/xen-api/vdi.store.ts'
@@ -36,6 +36,9 @@ export const useSrStore = defineStore('xen-api-sr', () => {
// TODO remove when the select component is ready to use
const getSrName = (ref: XenApiSr['$ref']) => baseContext.getByOpaqueRef(ref)?.name_label
const getSrsForHost = (hostRef: XenApiHost['$ref']) =>
baseContext.getByOpaqueRefs(pbdContext.getPbdsForHost(hostRef).map(pbd => pbd.SR))
const isDefaultSr = (sr: XenApiSr, pool: XenApiPool) =>
pool.default_SR !== 'OpaqueRef:NULL' && pool.default_SR === sr.$ref
@@ -68,6 +71,7 @@ export const useSrStore = defineStore('xen-api-sr', () => {
const context = {
...baseContext,
getAllocationStrategy,
getSrsForHost,
isDefaultSr,
isHaSr,
vdiIsosBySrName,

View File

@@ -1,11 +1,11 @@
<template>
<VtsQuickInfoCard class="host-dashboard-quick-info" :loading="!areHostsReady">
<VtsQuickInfoCard :loading="!areHostsReady" class="host-dashboard-quick-info">
<VtsKeyValueList>
<VtsKeyValueRow :label="t('state')">
<template #value>
<span class="power-state">
<VtsIcon :name="powerState.icon" size="medium" />
{{ powerState.text }}
<VtsIcon :name="hostStateDisplay.icon" size="medium" />
{{ hostStateDisplay.text }}
</span>
</template>
</VtsKeyValueRow>
@@ -55,10 +55,8 @@ import VtsKeyValueRow from '@core/components/key-value-row/VtsKeyValueRow.vue'
import VtsQuickInfoCard from '@core/components/quick-info-card/VtsQuickInfoCard.vue'
import VtsTag from '@core/components/tag/VtsTag.vue'
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
import { getRelativeTime } from '@core/composables/relative-time.composable.ts'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { formatSizeRaw } from '@core/utils/size.util.ts'
import { parseDateTime } from '@core/utils/time.util.ts'
import { HOST_POWER_STATE } from '@vates/types'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -67,18 +65,12 @@ const { host } = defineProps<{
host: FrontXoHost
}>()
const { t, locale } = useI18n()
const { t } = useI18n()
const { getMasterHostByPoolId, isMasterHost, areHostsReady } = useXoHostCollection()
const { getHostStatus } = useXoHostUtils(() => host)
const { getHostStateDisplay, relativeStartTime } = useXoHostUtils(() => host)
const powerState = computed(() => getHostStatus(getHostState(host)))
const date = computed(() => (host.startTime === null ? undefined : new Date(parseDateTime(host.startTime * 1000))))
const relativeStartTime = computed(() =>
date.value === undefined ? undefined : getRelativeTime(date.value, locale.value)
)
const hostStateDisplay = computed(() => getHostStateDisplay(getHostState(host)))
const isMaster = computed(() => isMasterHost(host.id))

View File

@@ -3,7 +3,7 @@
<UiTitle>{{ t('hosts') }}</UiTitle>
<VtsQueryBuilder :model-value="filter" :schema @search="filter = $event" />
<div class="container">
<VtsTable :state :pagination-bindings sticky="right">
<VtsTable :pagination-bindings :state sticky="right">
<thead>
<HeadCells />
</thead>
@@ -17,7 +17,7 @@
</div>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import { useHostQueryBuilder } from '@/modules/host/composables/use-host-query-builder.composable.ts'
import { type FrontXoHost, useXoHostCollection } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { getHostIcon } from '@/modules/host/utils/xo-host.util.ts'
@@ -109,7 +109,7 @@ const { HeadCells, BodyCells } = useHostColumns({
host: r =>
r({
label: host.name_label,
to: `/host/${host.id}/dashboard`,
to: { name: '/host/[id]/dashboard', params: { id: host.id } },
icon: hostIcon.value,
rightIcon: rightIcon.value,
}),
@@ -123,17 +123,15 @@ const { HeadCells, BodyCells } = useHostColumns({
})
</script>
<style scoped lang="postcss">
.container,
<style lang="postcss" scoped>
.hosts-table {
display: flex;
flex-direction: column;
}
.hosts-table {
gap: 2.4rem;
.container {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
}

View File

@@ -2,22 +2,22 @@
<UiPanelCard class="host-info-card">
<UiPanelCardTitle
:id="host.id"
size="medium"
:icon="hostIcon"
:label="host.name_label"
:to="{ name: '/host/[id]/dashboard', params: { id: host.id } }"
:icon="hostIcon"
size="medium"
/>
<div class="content">
<VtsCardRowKeyValue>
<template #key>{{ t('state') }}</template>
<template #value>
<span class="value">
<VtsIcon :name="powerState.icon" size="medium" />
{{ powerState.text }}
<VtsIcon :name="hostStateDisplay.icon" size="medium" />
{{ hostStateDisplay.text }}
</span>
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue truncate align-top>
<VtsCardRowKeyValue align-top truncate>
<template #key>{{ t('description') }}</template>
<template #value>{{ host.name_description }}</template>
<template v-if="host.name_description" #addons>
@@ -45,7 +45,7 @@
<template #key>{{ t('pool') }}</template>
<template #value>
<div v-if="pool" class="value">
<UiLink :to="{ name: '/pool/[id]/dashboard', params: { id: pool.id } }" size="small" icon="object:pool">
<UiLink :to="{ name: '/pool/[id]/dashboard', params: { id: pool.id } }" icon="object:pool" size="small">
{{ pool.name_label }}
</UiLink>
</div>
@@ -73,7 +73,7 @@
<template #key>{{ t('started') }}</template>
<template #value>{{ relativeStartTime }}</template>
<template v-if="relativeStartTime" #addons>
<VtsCopyButton :value="String(relativeStartTime)" />
<VtsCopyButton :value="relativeStartTime" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue>
@@ -119,7 +119,7 @@ const { host } = defineProps<{
const { t } = useI18n()
const { getHostStatus, getRelativeStartTime } = useXoHostUtils(() => host)
const { getHostStateDisplay, relativeStartTime } = useXoHostUtils(() => host)
const { useGetPoolById } = useXoPoolCollection()
@@ -139,14 +139,12 @@ const noMissingPatches = computed(() => nMissingPatches.value === 0)
const hostState = computed(() => getHostState(host))
const powerState = computed(() => getHostStatus(hostState.value))
const hostStateDisplay = computed(() => getHostStateDisplay(hostState.value))
const hostIcon = computed(() => getHostIcon(host))
const relativeStartTime = computed(() => (host.startTime ? getRelativeStartTime(host.startTime) : undefined))
</script>
<style scoped lang="postcss">
<style lang="postcss" scoped>
.host-info-card {
.content {
display: flex;

View File

@@ -1,10 +1,10 @@
<template>
<UiPanelCard class="host-network-card">
<UiPanelCardTitle size="medium" :label="t('network')" />
<UiPanelCardTitle :label="t('network')" size="medium" />
<div class="content">
<VtsCardRowKeyValue>
<template #key>{{ t('management-ip') }}</template>
<template v-if="host.address" #value>{{ host.address }}</template>
<template #value>{{ host.address }}</template>
<template v-if="host.address" #addons>
<VtsCopyButton :value="host.address" />
</template>
@@ -75,7 +75,7 @@ const ipV6Addresses = computed(
)
</script>
<style scoped lang="postcss">
<style lang="postcss" scoped>
.host-network-card {
.content {
display: flex;

View File

@@ -3,7 +3,7 @@ import type { XoHostState } from '@/modules/host/utils/xo-host.util.ts'
import { getHostPendingStateOperation, getHostSmartRebootVmOperation } from '@/modules/host/utils/xo-host.util.ts'
import { useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import type { IconName } from '@core/icons'
import useRelativeTime from '@core/composables/relative-time.composable.ts'
import { useTimeAgo } from '@core/composables/locale-time-ago.composable.ts'
import { createMapper, useMapper } from '@core/packages/mapper'
import { parseDateTime } from '@core/utils/time.util.ts'
import { toComputed } from '@core/utils/to-computed.util.ts'
@@ -17,7 +17,7 @@ export function useXoHostUtils(rawHost: MaybeRefOrGetter<FrontXoHost>) {
const { vmsByHost } = useXoVmCollection()
const getHostStatus = createMapper<XoHostState, { text: string; icon: IconName | undefined }>(
const getHostStateDisplay = createMapper<XoHostState, { text: string; icon: IconName | undefined }>(
{
running: { text: t('host:status:running'), icon: 'status:running-circle' },
halted: { text: t('host:status:halted'), icon: 'status:halted-circle' },
@@ -27,11 +27,15 @@ export function useXoHostUtils(rawHost: MaybeRefOrGetter<FrontXoHost>) {
'unknown'
)
function getRelativeStartTime(startTime: number) {
const date = computed(() => new Date(parseDateTime(startTime * 1000)))
const hostTimeAgo = useTimeAgo(() => new Date(parseDateTime((host.value.startTime ?? 0) * 1000)))
return useRelativeTime(date)
}
const relativeStartTime = computed(() => {
if (!host.value.startTime) {
return undefined
}
return hostTimeAgo.value
})
const pendingStateOperation = computed(
() =>
@@ -58,8 +62,8 @@ export function useXoHostUtils(rawHost: MaybeRefOrGetter<FrontXoHost>) {
)
return {
getHostStatus,
getRelativeStartTime,
getHostStateDisplay,
relativeStartTime,
isChangingState,
currentOperation,
}

View File

@@ -34,50 +34,56 @@ function mountHostUtils(overrides: Partial<FrontXoHost> = {}) {
}).wrapper.vm
}
describe('getHostStatus', () => {
describe('getHostStateDisplay', () => {
it('maps the running state to its label and running-circle icon', () => {
const { getHostStatus, t } = mountHostUtils()
const { getHostStateDisplay, t } = mountHostUtils()
expect(getHostStatus('running')).toEqual({
expect(getHostStateDisplay('running')).toEqual({
text: t('host:status:running'),
icon: 'status:running-circle',
})
})
it('maps the halted state to its label and halted-circle icon', () => {
const { getHostStatus, t } = mountHostUtils()
const { getHostStateDisplay, t } = mountHostUtils()
expect(getHostStatus('halted')).toEqual({
expect(getHostStateDisplay('halted')).toEqual({
text: t('host:status:halted'),
icon: 'status:halted-circle',
})
})
it('maps the disabled state to its label and disabled-circle icon', () => {
const { getHostStatus, t } = mountHostUtils()
const { getHostStateDisplay, t } = mountHostUtils()
expect(getHostStatus('disabled')).toEqual({
expect(getHostStateDisplay('disabled')).toEqual({
text: t('host:status:disabled'),
icon: 'status:host-disabled-circle',
})
})
it('maps the unknown state to its label and no icon', () => {
const { getHostStatus, t } = mountHostUtils()
const { getHostStateDisplay, t } = mountHostUtils()
expect(getHostStatus('unknown')).toEqual({ text: t('host:status:unknown'), icon: undefined })
expect(getHostStateDisplay('unknown')).toEqual({ text: t('host:status:unknown'), icon: undefined })
})
it('falls back to the unknown entry for an out-of-range value', () => {
const { getHostStatus, t } = mountHostUtils()
const { getHostStateDisplay, t } = mountHostUtils()
const outOfRangeState = 'Suspended' as XoHostState
expect(getHostStatus(outOfRangeState)).toEqual({ text: t('host:status:unknown'), icon: undefined })
expect(getHostStateDisplay(outOfRangeState)).toEqual({ text: t('host:status:unknown'), icon: undefined })
})
})
describe('getRelativeStartTime', () => {
describe('relativeStartTime', () => {
it('is undefined when the host has no start time', () => {
const { relativeStartTime } = mountHostUtils({ startTime: null })
expect(relativeStartTime).toBeUndefined()
})
it('reports the start time relative to now, reading it as seconds', () => {
const startTimeInSeconds = 1660000000
@@ -85,7 +91,7 @@ describe('getRelativeStartTime', () => {
const { locale } = useI18n()
return {
relativeStartTime: useXoHostUtils(createHost()).getRelativeStartTime(startTimeInSeconds),
relativeStartTime: useXoHostUtils(createHost({ startTime: startTimeInSeconds })).relativeStartTime,
expectedRelativeTime: getRelativeTime(new Date(startTimeInSeconds * 1000), locale.value),
}
})