feat(xo6): sr dedicated page hosts tab (#10140)

* Update CHANGELOG.unreleased.md

* Hosts tab

* Hosts side panel

* Update CHANGELOG.unreleased.md

* Route + fix import

* The dev routes

* Simplify SrHeader

* Refactoring

* Fix type

* locale

* Simplify pbd connection status

* Refactor srs get

* New import rules

* Fixes

* Singular locale

* Refactor + sr status

* Push generated routes

* Switch order

* PR Fixes

* Comment fixes

* Changelog packages

* Cleanup changelog

* Keep unreleased
This commit is contained in:
amouillard-vates
2026-09-08 10:04:07 +02:00
committed by GitHub
parent ead446fcdb
commit 069bb79d4a
18 changed files with 372 additions and 138 deletions

View File

@@ -50,7 +50,7 @@ const state = useTableState({
})
const { HeadCells, BodyCells } = useHostColumns({
exclude: ['selectItem'],
exclude: ['selectItem', 'srStatus'],
body: (host: XenApiHost) => {
const state = isHostRunning(host) ? 'running' : 'halted'

View File

@@ -281,6 +281,7 @@
"confirm-vm-revert": "Confirm VM revert",
"connect": "Connect",
"connected": "Connected",
"connected-srs": "Connected SRs",
"connected-to-ip": "Connected to {ip}",
"connecting": "Connecting",
"connection": "Connection",
@@ -841,6 +842,7 @@
"no-selected-vm-can-be-migrated": "No selected VM can be migrated",
"no-server-detected": "No servers detected",
"no-snapshot-detected": "No snapshots detected",
"no-sr-connected": "No SRs connected",
"no-storage-repository-detected": "No storage repositories detected",
"no-task": "No tasks",
"no-task-detected": "No tasks detected",

View File

@@ -281,6 +281,7 @@
"confirm-vm-revert": "Confirmer la restauration de la VM à l'instantané",
"connect": "Se connecter",
"connected": "Connecté",
"connected-srs": "SRs connectés",
"connected-to-ip": "Connecté à {ip}",
"connecting": "Connexion en cours",
"connection": "Connexion",
@@ -841,6 +842,7 @@
"no-selected-vm-can-be-migrated": "Aucune VM sélectionnée ne peut être migrée",
"no-server-detected": "Aucun serveur détecté",
"no-snapshot-detected": "Aucun instantané détecté",
"no-sr-connected": "Aucun SR connecté",
"no-storage-repository-detected": "Aucun dépôt de stockage détecté",
"no-task": "Aucune tâche",
"no-task-detected": "Aucune tâche détectée",

View File

@@ -2,6 +2,7 @@ import { defineColumns } from '@core/packages/table/define-columns.ts'
import { useAddressColumn } from '@core/tables/column-definitions/address-column.ts'
import { useLinkColumn } from '@core/tables/column-definitions/link-column.ts'
import { useSelectItemColumn } from '@core/tables/column-definitions/select-item-column.ts'
import { useStatusColumn } from '@core/tables/column-definitions/status-column.ts'
import { useTagColumn } from '@core/tables/column-definitions/tag-column.ts'
import { useTruncatedTextColumn } from '@core/tables/column-definitions/truncated-text-column.ts'
import { useI18n } from 'vue-i18n'
@@ -14,6 +15,7 @@ export const useHostColumns = defineColumns(() => {
description: useTruncatedTextColumn({ headerLabel: () => t('description') }),
ipAddresses: useAddressColumn({ headerLabel: () => t('management-ip') }),
tags: useTagColumn({ headerLabel: () => t('tags') }),
srStatus: useStatusColumn({ headerLabel: () => t('status') }),
selectItem: useSelectItemColumn(),
}
})

View File

@@ -19,8 +19,11 @@
<script setup lang="ts">
import { useXoHostCollection, type FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { getPbdsConnectionStatus } from '@/modules/pbd/utils/xo-pbd.util.ts'
import { useXoPifCollection } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import { getHostIpAddresses } from '@/modules/pif/utils/xo-pif.util.ts'
import { useGetPbdsInScope } from '@/modules/storage-repository/composables/xo-sr-utils.composable.ts'
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import VtsQueryBuilder from '@core/components/query-builder/VtsQueryBuilder.vue'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
@@ -32,6 +35,8 @@ import { icon, objectIcon } from '@core/icons'
import { useQueryBuilderSchema } from '@core/packages/query-builder/schema/use-query-builder-schema.ts'
import { useQueryBuilderFilter } from '@core/packages/query-builder/use-query-builder-filter.ts'
import { useHostColumns } from '@core/tables/column-sets/host-columns.ts'
import { CONNECTION_STATUS } from '@core/types/connection.ts'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import { useStringSchema } from '@core/utils/query-builder/use-string-schema.ts'
import { HOST_POWER_STATE } from '@vates/types'
import { logicAnd, logicNot, logicOr } from '@vueuse/math'
@@ -43,10 +48,12 @@ const {
busy,
hosts: rawHosts,
error,
sr,
} = defineProps<{
hosts: FrontXoHost[]
busy?: boolean
error?: boolean
sr?: FrontXoSr
}>()
const { t } = useI18n()
@@ -85,6 +92,14 @@ const state = useTableState({
const { pageRecords: paginatedHosts, paginationBindings } = usePagination('hosts', filteredHosts)
const { getPbdsInScope } = useGetPbdsInScope()
function getSrConnectionStatus(sr: FrontXoSr, host: FrontXoHost) {
const scope: SrScope = { type: SR_SCOPE_TYPE.HOST, hostId: host.id }
return getPbdsConnectionStatus(getPbdsInScope(sr, scope))
}
function getMasterIcon(host: FrontXoHost) {
if (!isMasterHost(host.id)) {
return undefined
@@ -97,10 +112,14 @@ function getMasterIcon(host: FrontXoHost) {
}
const { HeadCells, BodyCells } = useHostColumns({
exclude: sr === undefined ? ['srStatus'] : [],
body: (host: FrontXoHost) => {
const ipAddresses = computed(() => getHostIpAddresses(host.address, pifsByHost.value.get(host.id)))
const hostIcon = computed(() => objectIcon('host', toLower(host.power_state)))
const rightIcon = computed(() => getMasterIcon(host))
const srConnectionStatus = computed(() =>
sr === undefined ? CONNECTION_STATUS.DISCONNECTED : getSrConnectionStatus(sr, host)
)
return {
host: r =>
@@ -113,6 +132,7 @@ const { HeadCells, BodyCells } = useHostColumns({
description: r => r(host.name_description),
ipAddresses: r => r(ipAddresses.value),
tags: r => r(host.tags),
srStatus: r => r(srConnectionStatus.value),
selectItem: r => r(() => (selectedHostId.value = host.id)),
}
},

View File

@@ -2,6 +2,7 @@
<VtsSidePanel :has-selection="!!host" @close="emit('close')">
<template v-if="host">
<HostInfoCard :host />
<HostSrsCard :host />
<HostNetworkCard :host />
<!-- host licensing -->
<HostSoftwareCard :host />
@@ -16,6 +17,7 @@ import HostHardwareSpecificationsCard from '@/modules/host/components/list/panel
import HostInfoCard from '@/modules/host/components/list/panel/card/HostInfoCard.vue'
import HostNetworkCard from '@/modules/host/components/list/panel/card/HostNetworkCard.vue'
import HostSoftwareCard from '@/modules/host/components/list/panel/card/HostSoftwareCard.vue'
import HostSrsCard from '@/modules/host/components/list/panel/card/HostSrsCard.vue'
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'

View File

@@ -0,0 +1,74 @@
<template>
<UiPanelCard>
<UiCardTitle>
{{ t('connected-srs') }}
<UiCounter :value="srs.length" accent="neutral" size="small" variant="primary" />
</UiCardTitle>
<VtsStateHero v-if="!isReady" format="card" type="busy" size="extra-small" />
<VtsStateHero v-else-if="hasFetchError" 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.id" v-tooltip class="text-ellipsis">
<UiLink
size="small"
:icon="getSrStatusIcon(sr)"
:to="{
name: '/sr/[id]/general',
params: { id: sr.id },
query: toSrScopeQuery(scope),
}"
>
{{ 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 { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPbdCollection } from '@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'
import { useGetPbdsInScope, useXoSrUtils } from '@/modules/storage-repository/composables/xo-sr-utils.composable.ts'
import { useXoSrCollection } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import { toSrScopeQuery } from '@/modules/storage-repository/utils/sr-scope.util.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import UiCollapsibleList from '@core/components/ui/collapsible-list/UiCollapsibleList.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.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: FrontXoHost
}>()
const { t } = useI18n()
const { srsByHost, areSrsReady, hasSrFetchError } = useXoSrCollection()
const { arePbdsReady, hasPbdFetchError } = useXoPbdCollection()
const isReady = logicAnd(areSrsReady, arePbdsReady)
const hasFetchError = logicOr(hasSrFetchError, hasPbdFetchError)
const scope = computed<SrScope>(() => ({ type: SR_SCOPE_TYPE.HOST, hostId: host.id }))
const { isConnectedInScope } = useGetPbdsInScope()
const { getSrStatusIcon } = useXoSrUtils(undefined, scope)
const srs = computed(() => {
const hostSrs = srsByHost.value.get(host.id) ?? []
return hostSrs.filter(sr => isConnectedInScope(sr, scope.value))
})
</script>

View File

@@ -1,5 +1,5 @@
<template>
<SrHeaderBreadcrumbLink :sr :host :from-context />
<SrHeaderBreadcrumb :sr :scope />
<UiHeadBar>
<template #icon>
<VtsObjectIcon type="sr" :state="srIconState" size="medium" />
@@ -12,46 +12,55 @@
<TabList>
<RouterLink
v-slot="{ isActive, href }"
:to="{ name: '/sr/[id]/general', params: { id: sr.id }, query: route.query }"
:to="{ name: '/sr/[id]/general', params: { id: sr.id }, query: scopeQuery }"
custom
>
<TabItem :active="isActive" :href tag="a">
{{ t('general') }}
</TabItem>
</RouterLink>
<RouterLink
v-slot="{ isActive, href }"
:to="{ name: '/sr/[id]/hosts', params: { id: sr.id }, query: scopeQuery }"
custom
>
<TabItem :active="isActive" :href tag="a">
{{ t('hosts') }}
</TabItem>
</RouterLink>
</TabList>
</template>
<script setup lang="ts">
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPbdCollection } from '@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'
import SrHeaderBreadcrumbLink from '@/modules/storage-repository/components/header/SrHeaderBreadcrumbLink.vue'
import SrHeaderBreadcrumb from '@/modules/storage-repository/components/header/SrHeaderBreadcrumb.vue'
import { useXoSrUtils } from '@/modules/storage-repository/composables/xo-sr-utils.composable.ts'
import { useXoSrCollection } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import { toSrScopeQuery } from '@/modules/storage-repository/utils/sr-scope.util.ts'
import type { SrScope } from '@core/types/storage-repository.type.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
import TabItem from '@core/components/tab/TabItem.vue'
import TabList from '@core/components/tab/TabList.vue'
import UiHeadBar from '@core/components/ui/head-bar/UiHeadBar.vue'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
const { sr, fromContext } = defineProps<{ sr: FrontXoSr; host?: FrontXoHost; fromContext?: SrScope }>()
const { sr, scope } = defineProps<{ sr: FrontXoSr; scope: SrScope }>()
const { t } = useI18n()
const route = useRoute()
const { arePbdsReady } = useXoPbdCollection()
const { isDefaultSr } = useXoSrCollection()
const scopeQuery = computed(() => toSrScopeQuery(scope))
const { srConnectionStatus } = useXoSrUtils(
() => sr,
() => fromContext ?? { type: SR_SCOPE_TYPE.POOL }
() => scope
)
const srIconState = computed(() => (arePbdsReady.value ? srConnectionStatus.value : undefined))

View File

@@ -0,0 +1,115 @@
<template>
<div v-if="parent" class="sr-header-breadcrumb">
<UiBreadcrumb :size>
<UiLink :size :to="parent.dashboardTo" :icon="parent.icon">
{{ parent.label }}
</UiLink>
<UiLink :size :to="parent.storageTo">
{{ t('storage') }}
</UiLink>
<span class="sr-name">
<VtsObjectIcon type="sr" :state="srConnectionState" size="current" />
{{ sr.name_label }}
</span>
</UiBreadcrumb>
</div>
</template>
<script setup lang="ts">
import { useXoHostCollection, type FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPbdCollection } from '@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'
import { useXoPoolCollection } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
import { useXoSrUtils } from '@/modules/storage-repository/composables/xo-sr-utils.composable.ts'
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
import UiBreadcrumb from '@core/components/ui/breadcrumb/UiBreadcrumb.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import { objectIcon, type IconName } from '@core/icons'
import { useUiStore } from '@core/stores/ui.store.ts'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import { toLower } from 'lodash-es'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { RouteLocationRaw } from 'vue-router'
type SrBreadcrumbParent = {
icon: IconName
label: string
dashboardTo: RouteLocationRaw
storageTo: RouteLocationRaw
}
const { sr, scope } = defineProps<{ sr: FrontXoSr; scope: SrScope }>()
const { t } = useI18n()
const uiStore = useUiStore()
const { useGetHostById } = useXoHostCollection()
const { useGetPoolById } = useXoPoolCollection()
const size = computed(() => (uiStore.isSmall ? 'small' : 'medium'))
const host = useGetHostById(() => (scope.type === SR_SCOPE_TYPE.HOST ? (scope.hostId as FrontXoHost['id']) : undefined))
const pool = useGetPoolById(() => sr.$pool)
const { arePbdsReady } = useXoPbdCollection()
const parent = computed<SrBreadcrumbParent | undefined>(() => {
if (scope.type === SR_SCOPE_TYPE.HOST) {
const scopedHost = host.value
if (scopedHost === undefined) {
return undefined
}
return {
icon: objectIcon('host', toLower(scopedHost.power_state)),
label: scopedHost.name_label,
dashboardTo: { name: '/host/[id]/dashboard', params: { id: scopedHost.id } },
storageTo: { name: '/host/[id]/storage', params: { id: scopedHost.id } },
}
}
const srPool = pool.value
if (srPool === undefined) {
return undefined
}
return {
icon: 'object:pool',
label: srPool.name_label,
dashboardTo: { name: '/pool/[id]/dashboard', params: { id: srPool.id } },
storageTo: { name: '/pool/[id]/storage', params: { id: srPool.id } },
}
})
const { srConnectionStatus } = useXoSrUtils(
() => sr,
() => scope
)
const srConnectionState = computed(() => (arePbdsReady.value ? srConnectionStatus.value : undefined))
</script>
<style lang="postcss" scoped>
.sr-header-breadcrumb {
min-height: 5.6rem;
padding: 1.2rem 1.6rem;
display: flex;
gap: 1.6rem;
align-items: center;
border-bottom: 0.1rem solid var(--color-neutral-border);
background-color: var(--color-neutral-background-primary);
justify-content: space-between;
overflow-y: auto;
.sr-name {
display: flex;
align-items: center;
gap: 1rem;
}
}
</style>

View File

@@ -1,91 +0,0 @@
<template>
<div class="breadcrumb-container">
<UiBreadcrumb v-if="fromContext?.type === SR_SCOPE_TYPE.HOST && host" :size>
<UiLink :size :to="{ name: '/host/[id]/dashboard', params: { id: host.id } }">
<VtsObjectIcon type="host" :state="toLower(host.power_state)" size="current" />
{{ host.name_label }}
</UiLink>
<UiLink :size :to="{ name: '/host/[id]/storage', params: { id: host.id } }">
{{ t('storage') }}
</UiLink>
<span class="sr-name">
<VtsObjectIcon type="sr" :state="srIconState" size="current" />
{{ sr.name_label }}
</span>
</UiBreadcrumb>
<UiBreadcrumb v-else-if="fromContext?.type === SR_SCOPE_TYPE.POOL && pool" :size>
<UiLink :size :to="{ name: '/pool/[id]/dashboard', params: { id: pool.id } }">
<VtsIcon name="object:pool" size="current" />
{{ pool.name_label }}
</UiLink>
<UiLink :size :to="{ name: '/pool/[id]/storage', params: { id: pool.id } }">
{{ t('storage') }}
</UiLink>
<span class="sr-name">
<VtsObjectIcon type="sr" :state="srIconState" size="current" />
{{ sr.name_label }}
</span>
</UiBreadcrumb>
<UiBreadcrumb v-else :size>
<span class="sr-name">
<VtsObjectIcon type="sr" :state="srIconState" size="current" />
{{ sr.name_label }}
</span>
</UiBreadcrumb>
</div>
</template>
<script setup lang="ts">
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPbdCollection } from '@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'
import { useXoPoolCollection } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
import { useXoSrUtils } from '@/modules/storage-repository/composables/xo-sr-utils.composable.ts'
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
import UiBreadcrumb from '@core/components/ui/breadcrumb/UiBreadcrumb.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import { useUiStore } from '@core/stores/ui.store.ts'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import { toLower } from 'lodash-es'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { sr, fromContext } = defineProps<{ sr: FrontXoSr; host?: FrontXoHost; fromContext?: SrScope }>()
const { t } = useI18n()
const uiStore = useUiStore()
const size = computed(() => (uiStore.isSmall ? 'small' : 'medium'))
const { useGetPoolById } = useXoPoolCollection()
const pool = useGetPoolById(() => sr.$pool)
const { arePbdsReady } = useXoPbdCollection()
const { srConnectionStatus } = useXoSrUtils(
() => sr,
() => fromContext ?? { type: SR_SCOPE_TYPE.POOL }
)
const srIconState = computed(() => (arePbdsReady.value ? srConnectionStatus.value : undefined))
</script>
<style lang="postcss" scoped>
.breadcrumb-container {
min-height: 5.6rem;
padding: 1.2rem 1.6rem;
display: flex;
gap: 1.6rem;
align-items: center;
border-bottom: 0.1rem solid var(--color-neutral-border);
background-color: var(--color-neutral-background-primary);
justify-content: space-between;
overflow-y: auto;
.sr-name {
display: flex;
align-items: center;
gap: 1rem;
}
}
</style>

View File

@@ -1,6 +1,7 @@
import { useXoHostCollection } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPbdUtils } from '@/modules/pbd/composables/xo-pbd-utils.composable.ts'
import { useXoPbdCollection, type FrontXoPbd } from '@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'
import { getPbdsConnectionStatus } from '@/modules/pbd/utils/xo-pbd.util.ts'
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import { type IconName, objectIcon } from '@core/icons'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
@@ -33,7 +34,7 @@ export function useGetPbdsInScope() {
function getSrPbdsSignature(sr: FrontXoSr | undefined, scope: SrScope) {
if (sr === undefined) {
return scope.type === 'host' ? `host:${scope.hostId}` : 'pool'
return scope.type === SR_SCOPE_TYPE.HOST ? `host:${scope.hostId}` : 'pool'
}
const scopedPbds = getPbdsInScope(sr, scope)
@@ -41,6 +42,10 @@ export function useGetPbdsInScope() {
return scopedPbds.map(pbd => `${pbd.id}:${pbd.attached}`).join('|') || sr.id
}
function isConnectedInScope(sr: FrontXoSr, scope: SrScope) {
return getPbdsInScope(sr, scope).some(pbd => pbd.attached)
}
function isPartiallyConnectedInScope(sr: FrontXoSr, scope: SrScope) {
const pbds = getPbdsInScope(sr, scope)
@@ -52,6 +57,7 @@ export function useGetPbdsInScope() {
getAttachedPbdsInScope,
getDetachedPbdsInScope,
getSrPbdsSignature,
isConnectedInScope,
isPartiallyConnectedInScope,
}
}
@@ -89,6 +95,10 @@ export function useXoSrUtils(
const srStatusIcon = computed<IconName>(() => objectIcon('sr', allPbdsConnectionStatus.value))
function getSrStatusIcon(sr: FrontXoSr): IconName {
return objectIcon('sr', getPbdsConnectionStatus(getPbdsInScope(sr, scope.value)))
}
function getSrLocation(sr: FrontXoSr): string {
if (sr.shared) {
return t('shared')
@@ -115,6 +125,7 @@ export function useXoSrUtils(
srConnectionStatus: allPbdsConnectionStatus,
isPartiallyConnectedInScope,
srStatusIcon,
getSrStatusIcon,
getSrLocation,
getSrAccessModeLabel,
getSrProvisioningLabel,

View File

@@ -1,3 +1,5 @@
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPbdCollection } from '@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'
import { useXoPoolCollection, type FrontXoPool } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
import { useXoVdiCollection, type FrontXoVdi } from '@/modules/vdi/remote-resources/use-xo-vdi-collection.ts'
import { useWatchCollection } from '@/shared/composables/watch-collection.composable.ts'
@@ -8,7 +10,7 @@ import { defineRemoteResource } from '@core/packages/remote-resource/define-remo
import { sortByNameLabel } from '@core/utils/sort-by-name-label.util.ts'
import type { XoSr } from '@vates/types'
import { reactify, useSorted } from '@vueuse/core'
import { ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
export type FrontXoSr = Pick<XoSr, (typeof srFields)[number]>
@@ -43,6 +45,7 @@ export const useXoSrCollection = defineRemoteResource({
const { getVdiById } = useXoVdiCollection(context)
const { getPoolById } = useXoPoolCollection()
const { pbdsBySr } = useXoPbdCollection()
const state = useXoCollectionState(sortedSrs, {
context,
@@ -77,6 +80,18 @@ export const useXoSrCollection = defineRemoteResource({
srsByPool.value = tmpSrsByPool
})
const srsByHost = computed(() => {
const tmpSrsByHost = new Map<FrontXoHost['id'], FrontXoSr[]>()
sortedSrs.value.forEach(sr => {
pbdsBySr.value.get(sr.id)?.forEach(pbd => {
safePushInMap(tmpSrsByHost, pbd.host, sr)
})
})
return tmpSrsByHost
})
const isDefaultSr = (sr: FrontXoSr) => getPoolById(sr.$pool)?.default_SR === sr.id
const isHighAvailabilitySr = reactify((sr: FrontXoSr) => {
@@ -89,6 +104,7 @@ export const useXoSrCollection = defineRemoteResource({
...state,
vdiIsosBySrName,
srsByPool,
srsByHost,
isDefaultSr,
isHighAvailabilitySr,
}

View File

@@ -0,0 +1,27 @@
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import type { LocationQuery } from 'vue-router'
export type SrScopeQuery =
| { from: typeof SR_SCOPE_TYPE.POOL }
| { from: typeof SR_SCOPE_TYPE.HOST; host: FrontXoHost['id'] }
export function toSrScopeQuery(scope: SrScope): SrScopeQuery {
if (scope.type === SR_SCOPE_TYPE.HOST) {
return { from: SR_SCOPE_TYPE.HOST, host: scope.hostId as FrontXoHost['id'] }
}
return { from: SR_SCOPE_TYPE.POOL }
}
export function parseSrScopeQuery(query: LocationQuery): SrScope {
const { from, host } = query
// `?host` alone yields null and a repeated `?host=a&host=b` yields an array,
// so the id is only trusted once narrowed to a string
if (from === SR_SCOPE_TYPE.HOST && typeof host === 'string') {
return { type: SR_SCOPE_TYPE.HOST, hostId: host as FrontXoHost['id'] }
}
return { type: SR_SCOPE_TYPE.POOL }
}

View File

@@ -1,7 +1,7 @@
<template>
<VtsContentSidePanel class="storage">
<UiCard class="container">
<StorageRepositoriesTable :srs :busy="!isReady" :error="hasSrFetchError" :scope />
<StorageRepositoriesTable :srs :busy="!isReady" :error="hasFetchError" :scope />
</UiCard>
<StorageRepositorySidePanel :sr="selectedSr" :scope @close="selectedSr = undefined" />
</VtsContentSidePanel>
@@ -20,34 +20,21 @@ import VtsContentSidePanel from '@core/components/layout/VtsContentSidePanel.vue
import UiCard from '@core/components/ui/card/UiCard.vue'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import { sortByNameLabel } from '@core/utils/sort-by-name-label.util.ts'
import { logicAnd } from '@vueuse/math'
import { logicAnd, logicOr } from '@vueuse/math'
import { computed } from 'vue'
const { host } = defineProps<{
host: FrontXoHost
}>()
const { hasSrFetchError, getSrById, areSrsReady } = useXoSrCollection()
const { pbdsByHost, arePbdsReady } = useXoPbdCollection()
const { srsByHost, getSrById, areSrsReady, hasSrFetchError } = useXoSrCollection()
const { arePbdsReady, hasPbdFetchError } = useXoPbdCollection()
const isReady = logicAnd(areSrsReady, arePbdsReady)
const srs = computed(() => {
const hostPbds = pbdsByHost.value.get(host.id) ?? []
const hasFetchError = logicOr(hasSrFetchError, hasPbdFetchError)
return hostPbds
.reduce<FrontXoSr[]>((acc, pbd) => {
const sr = getSrById(pbd.SR)
if (sr !== undefined) {
acc.push(sr)
}
return acc
}, [])
.sort((sr1, sr2) => sortByNameLabel(sr1, sr2))
})
const srs = computed(() => srsByHost.value.get(host.id) ?? [])
const selectedSr = useRouteQuery<FrontXoSr | undefined>('id', {
toData: id => getSrById(id as FrontXoSr['id']),

View File

@@ -4,22 +4,21 @@
{{ t('object-not-found', { id: route.params.id }) }}
</VtsStateHero>
<RouterView v-else v-slot="{ Component }">
<SrHeader v-if="uiStore.hasUi" :sr :host :from-context />
<SrHeader v-if="uiStore.hasUi" :sr :scope />
<component :is="Component" :sr />
</RouterView>
</template>
<script lang="ts" setup>
import { useXoHostCollection, type FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import SrHeader from '@/modules/storage-repository/components/SrHeader.vue'
import SrHeader from '@/modules/storage-repository/components/header/SrHeader.vue'
import {
type FrontXoSr,
useXoSrCollection,
} from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import { parseSrScopeQuery } from '@/modules/storage-repository/utils/sr-scope.util.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import { useDefaultTab } from '@core/composables/default-tab.composable.ts'
import { useUiStore } from '@core/stores/ui.store.ts'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
@@ -36,15 +35,5 @@ const { areSrsReady, useGetSrById } = useXoSrCollection()
const sr = useGetSrById(() => route.params.id as FrontXoSr['id'])
const fromContext = computed<SrScope>(() =>
route.query.from === SR_SCOPE_TYPE.HOST && route.query.host
? { type: SR_SCOPE_TYPE.HOST, hostId: route.query.host as FrontXoHost['id'] }
: { type: SR_SCOPE_TYPE.POOL }
)
const { useGetHostById } = useXoHostCollection()
const host = useGetHostById(() =>
fromContext.value.type === SR_SCOPE_TYPE.HOST ? (fromContext.value.hostId as FrontXoHost['id']) : undefined
)
const scope = computed(() => parseSrScopeQuery(route.query))
</script>

View File

@@ -0,0 +1,52 @@
<template>
<VtsContentSidePanel class="hosts">
<UiCard class="container">
<HostsTable :hosts :sr :busy="!isReady" :error="hasError" />
</UiCard>
<HostSidePanel :host="selectedHost" @close="selectedHost = undefined" />
</VtsContentSidePanel>
</template>
<script lang="ts" setup>
import HostsTable from '@/modules/host/components/list/HostsTable.vue'
import HostSidePanel from '@/modules/host/components/list/panel/HostSidePanel.vue'
import { useXoHostCollection, type FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPbdCollection } from '@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import VtsContentSidePanel from '@core/components/layout/VtsContentSidePanel.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
import { logicAnd, logicOr } from '@vueuse/math'
import { computed } from 'vue'
const { sr } = defineProps<{
sr: FrontXoSr
}>()
const { getHostById, areHostsReady, hasHostFetchError } = useXoHostCollection()
const { pbdsBySr, arePbdsReady, hasPbdFetchError } = useXoPbdCollection()
const isReady = logicAnd(areHostsReady, arePbdsReady)
const hasError = logicOr(hasHostFetchError, hasPbdFetchError)
const hosts = computed(() =>
(pbdsBySr.value.get(sr.id) ?? [])
.map(pbd => getHostById(pbd.host))
.filter((host): host is FrontXoHost => host !== undefined)
)
const selectedHost = useRouteQuery<FrontXoHost | undefined>('id', {
toData: id => hosts.value.find(host => host.id === id),
toQuery: host => host?.id ?? '',
})
</script>
<style scoped lang="postcss">
.hosts {
.container {
height: fit-content;
gap: 4rem;
margin: 0.8rem;
}
}
</style>

View File

@@ -316,6 +316,7 @@ declare module 'vue-router/auto-routes' {
{ id: ParamValue<true> },
{ id: ParamValue<false> },
| '/sr/[id]/general'
| '/sr/[id]/hosts'
>,
'/sr/[id]/general': RouteRecordInfo<
'/sr/[id]/general',
@@ -324,6 +325,13 @@ declare module 'vue-router/auto-routes' {
{ id: ParamValue<false> },
| never
>,
'/sr/[id]/hosts': RouteRecordInfo<
'/sr/[id]/hosts',
'/sr/:id/hosts',
{ id: ParamValue<true> },
{ id: ParamValue<false> },
| never
>,
'/traffic-rule/new': RouteRecordInfo<
'/traffic-rule/new',
'/traffic-rule/new',
@@ -737,6 +745,7 @@ declare module 'vue-router/auto-routes' {
routes:
| '/sr/[id]'
| '/sr/[id]/general'
| '/sr/[id]/hosts'
views:
| 'default'
}
@@ -746,6 +755,12 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
'src/pages/sr/[id]/hosts.vue': {
routes:
| '/sr/[id]/hosts'
views:
| never
}
'src/pages/traffic-rule/new.vue': {
routes:
| '/traffic-rule/new'

View File

@@ -12,6 +12,7 @@
> Users must be able to say: "Nice enhancement, I'm eager to test it"
- [XO6] Allow changing which PIF a host uses for its management interface, without deleting and recreating the network config (PR [#10110](https://github.com/vatesfr/xen-orchestra/pull/10110))
- [XO6/SR] Add dedicated Storage Repository page hosts sidepanel (PR [#10140](https://github.com/vatesfr/xen-orchestra/pull/10140))
### Bug fixes
@@ -35,6 +36,7 @@
<!--packages-start-->
- @xen-orchestra/web minor
- @xen-orchestra/web-core minor
<!--packages-end-->