mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
feat(lite): add host side panel in pool page (#10580)
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>()
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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})` : '')
|
||||
}
|
||||
|
||||
7
@xen-orchestra/lite/src/modules/pif/utils/pif.util.ts
Normal file
7
@xen-orchestra/lite/src/modules/pif/utils/pif.util.ts
Normal 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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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', {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user