mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-10 22:14:48 -05:00
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:
@@ -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'
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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))
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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']),
|
||||
|
||||
@@ -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>
|
||||
|
||||
52
@xen-orchestra/web/src/pages/sr/[id]/hosts.vue
Normal file
52
@xen-orchestra/web/src/pages/sr/[id]/hosts.vue
Normal 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>
|
||||
15
@xen-orchestra/web/src/route-map.d.ts
vendored
15
@xen-orchestra/web/src/route-map.d.ts
vendored
@@ -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'
|
||||
|
||||
@@ -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-->
|
||||
|
||||
Reference in New Issue
Block a user