mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-10 22:14:48 -05:00
feat(lite): add storage tabs and read-only SR views (#10005)
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
- [XOA deploy] Update log visualization component (PR [#9995](https://github.com/vatesfr/xen-orchestra/pull/9995))
|
||||
- [SidePanels] Add and use new `VtsCardObjectTitle` component to display object title and ID in side panels (PR [#9755](https://github.com/vatesfr/xen-orchestra/pull/9755))
|
||||
- Replacement of the UiSpinner component with UiLoader from web-core (PR [#10023](https://github.com/vatesfr/xen-orchestra/pull/10023))
|
||||
- [Pool,Host/Storage] Add Storage tabs (PR [#10005](https://github.com/vatesfr/xen-orchestra/pull/10005))
|
||||
|
||||
## **0.22.0** (2026-05-28)
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<RouterTab :to="{ name: '/host/[uuid]/network', params: { uuid } }">
|
||||
{{ t('network') }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: '/host/[uuid]/storage', params: { uuid } }">
|
||||
{{ t('storage') }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: '/host/[uuid]/tasks', params: { uuid } }" disabled>
|
||||
{{ t('tasks') }}
|
||||
</RouterTab>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<RouterTab :to="{ name: '/pool/[uuid]/network', params: { uuid: pool?.uuid ?? '-' } }">
|
||||
{{ t('network') }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: '/pool/[uuid]/storage', params: { uuid: pool?.uuid ?? '-' } }" disabled>
|
||||
<RouterTab :to="{ name: '/pool/[uuid]/storage', params: { uuid: pool?.uuid ?? '-' } }">
|
||||
{{ t('storage') }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: '/pool/[uuid]/tasks', params: { uuid: pool?.uuid ?? '-' } }">
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import RouterTab from '@/components/RouterTab.vue'
|
||||
import { usePoolStore } from '@/stores/xen-api/pool.store'
|
||||
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
|
||||
import TabList from '@core/components/tab/TabList.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
<RouterTab :to="{ name: '/vm/[uuid]/network', params: { uuid } }">
|
||||
{{ t('network') }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: '/vm/[uuid]/storage', params: { uuid } }" disabled>
|
||||
{{ t('storage') }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: '/vm/[uuid]/tasks', params: { uuid } }" disabled>
|
||||
{{ t('tasks') }}
|
||||
</RouterTab>
|
||||
|
||||
33
@xen-orchestra/lite/src/libs/sr.ts
Normal file
33
@xen-orchestra/lite/src/libs/sr.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export const ALLOCATION_BY_SR_TYPE = {
|
||||
ext: 'thin',
|
||||
file: 'thin',
|
||||
hba: 'thick',
|
||||
iscsi: 'thick',
|
||||
lvhd: 'thick',
|
||||
lvhdofcoe: 'thick',
|
||||
lvhdohba: 'thick',
|
||||
lvhdoiscsi: 'thick',
|
||||
lvm: 'thick',
|
||||
lvmofcoe: 'thick',
|
||||
lvmohba: 'thick',
|
||||
lvmoiscsi: 'thick',
|
||||
nfs: 'thin',
|
||||
ocfs: 'thick',
|
||||
ocfsohba: 'thick',
|
||||
ocfsoiscsi: 'thick',
|
||||
rawhba: 'thick',
|
||||
rawiscsi: 'thick',
|
||||
shm: 'thin',
|
||||
smb: 'thin',
|
||||
udev: 'thick',
|
||||
zfs: 'thin',
|
||||
} as const
|
||||
|
||||
export type SrAllocationStrategy = 'thin' | 'thick' | 'unknown'
|
||||
|
||||
export function getAllocationStrategy(srType: string, pbdProvisioning?: string): SrAllocationStrategy | undefined {
|
||||
if (srType === 'linstor') {
|
||||
return (pbdProvisioning as SrAllocationStrategy) ?? undefined
|
||||
}
|
||||
return ALLOCATION_BY_SR_TYPE[srType as keyof typeof ALLOCATION_BY_SR_TYPE]
|
||||
}
|
||||
21
@xen-orchestra/lite/src/libs/vdi.ts
Normal file
21
@xen-orchestra/lite/src/libs/vdi.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { XenApiVbd, XenApiVdi } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import { type IconName, objectIcon } from '@core/icons'
|
||||
|
||||
export function getVdiIcon(vbds: XenApiVbd[]): IconName {
|
||||
if (vbds.length === 0 || vbds.every(vbd => !vbd.currently_attached)) {
|
||||
return objectIcon('vdi', 'detached')
|
||||
}
|
||||
|
||||
if (vbds.every(vbd => vbd.currently_attached)) {
|
||||
return objectIcon('vdi', 'attached')
|
||||
}
|
||||
|
||||
return objectIcon('vdi', 'warning')
|
||||
}
|
||||
|
||||
export function getVbdsForVdi(
|
||||
vdi: XenApiVdi,
|
||||
getVbdByOpaqueRef: (ref: XenApiVbd['$ref']) => XenApiVbd | undefined
|
||||
): XenApiVbd[] {
|
||||
return vdi.VBDs.map(ref => getVbdByOpaqueRef(ref)).filter((vbd): vbd is XenApiVbd => vbd !== undefined)
|
||||
}
|
||||
@@ -42,8 +42,8 @@ import type {
|
||||
VMSS_TYPE,
|
||||
VTPM_OPERATION,
|
||||
VUSB_OPERATION,
|
||||
} from '@/libs/xen-api/xen-api.enums'
|
||||
import type { XEN_API_OBJECT_TYPES } from '@/libs/xen-api/xen-api.utils'
|
||||
} from '@/libs/xen-api/xen-api.enums.ts'
|
||||
import type { XEN_API_OBJECT_TYPES } from '@/libs/xen-api/xen-api.utils.ts'
|
||||
import type { OPAQUE_REF_NULL } from '@vates/types'
|
||||
|
||||
type TypeMapping = typeof XEN_API_OBJECT_TYPES
|
||||
@@ -167,7 +167,10 @@ export interface XenApiHost extends XenApiRecord<'host'> {
|
||||
|
||||
export interface XenApiSr extends XenApiRecord<'sr'> {
|
||||
content_type: string
|
||||
name_description: string
|
||||
name_label: string
|
||||
other_config: Record<string, string>
|
||||
tags: string[]
|
||||
VDIs: XenApiVdi['$ref'][]
|
||||
PBDs: XenApiPbd['$ref'][]
|
||||
physical_size: number
|
||||
@@ -182,6 +185,7 @@ export interface XenApiSr extends XenApiRecord<'sr'> {
|
||||
export interface XenApiPbd extends XenApiRecord<'pbd'> {
|
||||
SR: XenApiSr['$ref']
|
||||
currently_attached: boolean
|
||||
device_config: Record<string, string>
|
||||
host: XenApiHost['$ref']
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="storage-repositories-table">
|
||||
<UiTitle>
|
||||
{{ t('storage-repositories') }}
|
||||
</UiTitle>
|
||||
<div class="container">
|
||||
<div class="table-actions">
|
||||
<UiQuerySearchBar @search="value => (searchQuery = value)" />
|
||||
</div>
|
||||
<VtsTable :state :pagination-bindings sticky="right">
|
||||
<thead>
|
||||
<tr>
|
||||
<HeadCells />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<VtsRow v-for="sr of paginatedSrs" :key="sr.uuid" :selected="selectedSrId === sr.uuid">
|
||||
<BodyCells :item="sr" />
|
||||
</VtsRow>
|
||||
</tbody>
|
||||
</VtsTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { XenApiPool, XenApiSr } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import { usePbdUtils } from '@/modules/storage-repository/composables/pbd-utils.composable.ts'
|
||||
import { usePbdStore } from '@/stores/xen-api/pbd.store.ts'
|
||||
import { useSrStore } from '@/stores/xen-api/sr.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, objectIcon } from '@core/icons'
|
||||
import { useSrColumns } from '@core/tables/column-sets/sr-columns.ts'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const {
|
||||
srs: rawSrs,
|
||||
pool,
|
||||
busy,
|
||||
error,
|
||||
} = defineProps<{
|
||||
srs: XenApiSr[]
|
||||
pool: XenApiPool
|
||||
busy?: boolean
|
||||
error?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { isReady, hasError, isDefaultSr } = useSrStore().subscribe()
|
||||
const { getPbdsForSr } = usePbdStore().subscribe()
|
||||
|
||||
const selectedSrId = useRouteQuery('id')
|
||||
|
||||
const searchQuery = ref('')
|
||||
|
||||
const filteredSrs = computed(() => {
|
||||
const searchTerm = searchQuery.value.trim().toLocaleLowerCase()
|
||||
|
||||
if (!searchTerm) {
|
||||
return rawSrs
|
||||
}
|
||||
|
||||
return rawSrs.filter(sr => Object.values(sr).some(value => String(value).toLocaleLowerCase().includes(searchTerm)))
|
||||
})
|
||||
|
||||
const state = useTableState({
|
||||
busy: () => busy ?? !isReady.value,
|
||||
error: () => error ?? hasError.value,
|
||||
empty: () => {
|
||||
if (rawSrs.length === 0) {
|
||||
return t('no-storage-repository-detected')
|
||||
}
|
||||
|
||||
if (filteredSrs.value.length === 0) {
|
||||
return { type: 'no-result' }
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
})
|
||||
|
||||
const { pageRecords: paginatedSrs, paginationBindings } = usePagination('srs', filteredSrs)
|
||||
|
||||
function getPrimaryIcon(sr: XenApiSr) {
|
||||
if (!isDefaultSr(sr, pool)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
icon: icon('status:primary-circle'),
|
||||
tooltip: t('default-storage-repository'),
|
||||
}
|
||||
}
|
||||
|
||||
const { HeadCells, BodyCells } = useSrColumns({
|
||||
body: (sr: XenApiSr) => {
|
||||
const rightIcon = computed(() => getPrimaryIcon(sr))
|
||||
|
||||
const { allPbdsConnectionStatus } = usePbdUtils(() => getPbdsForSr(sr.$ref))
|
||||
|
||||
return {
|
||||
storageRepository: r =>
|
||||
r({
|
||||
label: sr.name_label,
|
||||
icon: objectIcon('sr', allPbdsConnectionStatus.value),
|
||||
rightIcon: rightIcon.value,
|
||||
}),
|
||||
description: r => r(sr.name_description),
|
||||
storageFormat: r => r(sr.type),
|
||||
accessMode: r => r(sr.shared ? t('shared') : t('local')),
|
||||
usedSpace: r => r(sr.physical_utilisation, sr.physical_size),
|
||||
actions: r => r({ onClick: () => (selectedSrId.value = sr.uuid) }),
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.storage-repositories-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
|
||||
.container,
|
||||
.table-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<VtsSidePanel :has-selection="!!sr" @close="emit('close')">
|
||||
<template v-if="sr">
|
||||
<VtsStateHero v-if="!isReady" format="panel" type="busy" size="medium" />
|
||||
<template v-else>
|
||||
<StorageRepositoryInfosCard :pool :sr />
|
||||
<StorageRepositorySpaceCard :sr />
|
||||
<StorageRepositoryVdisCard :vdis :vdi-snapshots />
|
||||
<StorageRepositoryHostsCard :hosts />
|
||||
<StorageRepositoryPbdsCard :pbds />
|
||||
<StorageRepositoryCustomFieldsCard :custom-fields />
|
||||
</template>
|
||||
</template>
|
||||
</VtsSidePanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { XenApiHost, XenApiPool, XenApiSr, XenApiVdi } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import StorageRepositoryCustomFieldsCard from '@/modules/storage-repository/components/list/panel/cards/StorageRepositoryCustomFieldsCard.vue'
|
||||
import StorageRepositoryHostsCard from '@/modules/storage-repository/components/list/panel/cards/StorageRepositoryHostsCard.vue'
|
||||
import StorageRepositoryInfosCard from '@/modules/storage-repository/components/list/panel/cards/StorageRepositoryInfosCard.vue'
|
||||
import StorageRepositoryPbdsCard from '@/modules/storage-repository/components/list/panel/cards/StorageRepositoryPbdsCard.vue'
|
||||
import StorageRepositorySpaceCard from '@/modules/storage-repository/components/list/panel/cards/StorageRepositorySpaceCard.vue'
|
||||
import StorageRepositoryVdisCard from '@/modules/storage-repository/components/list/panel/cards/StorageRepositoryVdisCard.vue'
|
||||
import { useHostStore } from '@/stores/xen-api/host.store.ts'
|
||||
import { usePbdStore } from '@/stores/xen-api/pbd.store.ts'
|
||||
import { useVdiStore } from '@/stores/xen-api/vdi.store.ts'
|
||||
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import { logicAnd } from '@vueuse/math'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { sr, pool } = defineProps<{
|
||||
sr?: XenApiSr
|
||||
pool: XenApiPool
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const { getByOpaqueRef: getVdiByOpaqueRef, isReady: areVdisReady } = useVdiStore().subscribe()
|
||||
const { getByOpaqueRef: getHostByOpaqueRef, isReady: areHostsReady } = useHostStore().subscribe()
|
||||
const { getPbdsForSr, isReady: arePbdsReady } = usePbdStore().subscribe()
|
||||
|
||||
const isReady = logicAnd(areVdisReady, areHostsReady, arePbdsReady)
|
||||
|
||||
const allVdis = computed(() => {
|
||||
if (sr === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
return sr.VDIs.map(vdiRef => getVdiByOpaqueRef(vdiRef)).filter((vdi): vdi is XenApiVdi => vdi !== undefined)
|
||||
})
|
||||
|
||||
const vdis = computed(() => allVdis.value.filter(vdi => !vdi.is_a_snapshot))
|
||||
const vdiSnapshots = computed(() => allVdis.value.filter(vdi => vdi.is_a_snapshot))
|
||||
|
||||
const pbds = computed(() => {
|
||||
if (sr === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
return getPbdsForSr(sr.$ref)
|
||||
})
|
||||
|
||||
const hosts = computed(() =>
|
||||
pbds.value.map(pbd => getHostByOpaqueRef(pbd.host)).filter((host): host is XenApiHost => host !== undefined)
|
||||
)
|
||||
|
||||
const customFields = computed(() => {
|
||||
if (sr === undefined) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const prefix = 'XenCenter.CustomFields.'
|
||||
|
||||
return Object.entries(sr.other_config).reduce<Record<string, unknown>>((acc, [key, value]) => {
|
||||
if (key.startsWith(prefix)) {
|
||||
acc[key.slice(prefix.length)] = value
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>
|
||||
{{ t('host') }}
|
||||
</template>
|
||||
<template #value>
|
||||
<UiLink
|
||||
v-if="host !== undefined"
|
||||
size="small"
|
||||
icon="object:host"
|
||||
:to="{ name: '/host/[uuid]/dashboard', params: { uuid: host.uuid } }"
|
||||
>
|
||||
{{ host.name_label }}
|
||||
</UiLink>
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { XenApiPbd } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import { useHostStore } from '@/stores/xen-api/host.store.ts'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { pbd } = defineProps<{
|
||||
pbd: XenApiPbd
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { getByOpaqueRef: getHostByOpaqueRef } = useHostStore().subscribe()
|
||||
|
||||
const host = computed(() => getHostByOpaqueRef(pbd.host))
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<UiCard class="card-container">
|
||||
<UiCardTitle>
|
||||
{{ t('custom-fields') }}
|
||||
</UiCardTitle>
|
||||
<div class="content">
|
||||
<VtsStateHero
|
||||
v-if="Object.keys(customFields).length === 0"
|
||||
type="no-data"
|
||||
format="card"
|
||||
horizontal
|
||||
size="extra-small"
|
||||
>
|
||||
{{ t('no-custom-field-detected') }}
|
||||
</VtsStateHero>
|
||||
<VtsLabelValueList v-else :fields="customFields" />
|
||||
</div>
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import VtsLabelValueList from '@core/components/label-value-list/VtsLabelValueList.vue'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<{
|
||||
customFields: Record<string, unknown>
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.card-container {
|
||||
gap: 1.6rem;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<UiCard class="card-container">
|
||||
<UiCardTitle>
|
||||
{{ t('hosts') }}
|
||||
<UiCounter :value="hosts.length" accent="neutral" size="small" variant="primary" />
|
||||
</UiCardTitle>
|
||||
<UiCollapsibleList v-if="hosts.length > 0" tag="ul" :total-items="hosts.length">
|
||||
<li v-for="host in hosts" :key="host.uuid" v-tooltip class="text-ellipsis">
|
||||
<UiLink
|
||||
size="small"
|
||||
:icon="`object:host:${getHostPowerState(host)}`"
|
||||
:to="{ name: '/host/[uuid]/dashboard', params: { uuid: host.uuid } }"
|
||||
>
|
||||
{{ host.name_label }}
|
||||
</UiLink>
|
||||
</li>
|
||||
</UiCollapsibleList>
|
||||
<VtsStateHero v-else type="no-data" format="card" horizontal size="extra-small">
|
||||
{{ t('no-host-attached') }}
|
||||
</VtsStateHero>
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.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 { vTooltip } from '@core/directives/tooltip.directive.ts'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<{
|
||||
hosts: XenApiHost[]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { isHostRunning } = useHostMetricsStore().subscribe()
|
||||
|
||||
function getHostPowerState(host: XenApiHost) {
|
||||
return isHostRunning(host) ? 'running' : 'halted'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.card-container {
|
||||
gap: 1.6rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<UiCard class="card-container">
|
||||
<UiCardTitle>
|
||||
<div v-if="sr.name_label" class="title">
|
||||
<VtsIcon :name="`object:sr:${allPbdsConnectionStatus}`" size="medium" />
|
||||
{{ sr.name_label }}
|
||||
</div>
|
||||
</UiCardTitle>
|
||||
<div class="content">
|
||||
<VtsCodeSnippet :content="sr.uuid" copy />
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('status') }}</template>
|
||||
<template #value>
|
||||
<VtsStatus :status="allPbdsConnectionStatus" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue truncate align-top>
|
||||
<template #key>{{ t('description') }}</template>
|
||||
<template #value>{{ sr.name_description }}</template>
|
||||
<template v-if="sr.name_description" #addons>
|
||||
<VtsCopyButton :value="sr.name_description" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue align-top>
|
||||
<template #key>{{ t('tags') }}</template>
|
||||
<template #value>
|
||||
<UiTagsList v-if="sr.tags.length > 0">
|
||||
<VtsTag v-for="tag in sr.tags" :key="tag" :value="tag" />
|
||||
</UiTagsList>
|
||||
</template>
|
||||
<template v-if="sr.tags.length > 0" #addons>
|
||||
<VtsCopyButton :value="sr.tags.join(', ')" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('storage-format') }}</template>
|
||||
<template #value>{{ sr.type }}</template>
|
||||
<template #addons>
|
||||
<VtsCopyButton :value="sr.type" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('access-mode') }}</template>
|
||||
<template #value>{{ isSrSharedI18nValue }}</template>
|
||||
<template #addons>
|
||||
<VtsCopyButton :value="isSrSharedI18nValue" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('provisioning') }}</template>
|
||||
<template #value>{{ allocationStrategy }}</template>
|
||||
<template #addons>
|
||||
<VtsCopyButton :value="allocationStrategy" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('high-availability') }}</template>
|
||||
<template #value><VtsStatus :status="isHaSr" /></template>
|
||||
</VtsCardRowKeyValue>
|
||||
</div>
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { XenApiPool, XenApiSr } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import { usePbdUtils } from '@/modules/storage-repository/composables/pbd-utils.composable.ts'
|
||||
import { usePbdStore } from '@/stores/xen-api/pbd.store.ts'
|
||||
import { useSrStore } from '@/stores/xen-api/sr.store.ts'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsCodeSnippet from '@core/components/code-snippet/VtsCodeSnippet.vue'
|
||||
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
|
||||
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
||||
import VtsStatus from '@core/components/status/VtsStatus.vue'
|
||||
import VtsTag from '@core/components/tag/VtsTag.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
|
||||
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { sr, pool } = defineProps<{
|
||||
sr: XenApiSr
|
||||
pool: XenApiPool
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { getPbdsForSr } = usePbdStore().subscribe()
|
||||
const { isHaSr: isHaSrForPool, getAllocationStrategy } = useSrStore().subscribe()
|
||||
|
||||
const { allPbdsConnectionStatus } = usePbdUtils(() => getPbdsForSr(sr.$ref))
|
||||
|
||||
const isSrSharedI18nValue = computed(() => (sr.shared ? t('shared') : t('local')))
|
||||
|
||||
const allocationStrategy = computed(() => getAllocationStrategy(sr) ?? t('unknown'))
|
||||
|
||||
const isHaSr = computed(() => isHaSrForPool(sr, pool))
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.card-container {
|
||||
gap: 1.6rem;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<UiCard class="card-container">
|
||||
<UiCardTitle>
|
||||
{{ t('pbd-details') }}
|
||||
</UiCardTitle>
|
||||
<VtsStateHero v-if="pbds.length === 0" type="no-data" format="card" horizontal size="extra-small">
|
||||
{{ t('no-pbd-attached') }}
|
||||
</VtsStateHero>
|
||||
<template v-else>
|
||||
<div class="content">
|
||||
<VtsStatus :status="allPbdsConnectionStatus" />
|
||||
</div>
|
||||
<div v-if="areSomePbdsDisconnected" class="content">
|
||||
<template v-for="(pbd, index) in disconnectedPbds" :key="pbd.uuid">
|
||||
<VtsDivider v-if="index > 0" class="divider" type="stretch" />
|
||||
<span class="typo-body-bold-small subtitle">{{ t('disconnected-pbd-number', { n: index + 1 }) }}</span>
|
||||
<StorageRepositoryPbdHost :pbd />
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>
|
||||
{{ t('current-attach') }}
|
||||
</template>
|
||||
<template #value>
|
||||
<VtsStatus
|
||||
:status="pbd.currently_attached ? CONNECTION_STATUS.CONNECTED : CONNECTION_STATUS.DISCONNECTED"
|
||||
/>
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<UiLogEntryViewer
|
||||
v-if="Object.keys(pbd.device_config).length > 0"
|
||||
:content="pbd.device_config"
|
||||
:label="t('device-config')"
|
||||
size="small"
|
||||
accent="info"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { XenApiPbd } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import StorageRepositoryPbdHost from '@/modules/storage-repository/components/list/panel/card-items/StorageRepositoryPbdHost.vue'
|
||||
import { usePbdUtils } from '@/modules/storage-repository/composables/pbd-utils.composable.ts'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsDivider from '@core/components/divider/VtsDivider.vue'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import VtsStatus from '@core/components/status/VtsStatus.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
|
||||
import UiLogEntryViewer from '@core/components/ui/log-entry-viewer/UiLogEntryViewer.vue'
|
||||
import { CONNECTION_STATUS } from '@core/types/connection.ts'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { pbds } = defineProps<{
|
||||
pbds: XenApiPbd[]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { areSomePbdsDisconnected, allPbdsConnectionStatus, disconnectedPbds } = usePbdUtils(() => pbds)
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.card-container {
|
||||
gap: 1.6rem;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
|
||||
.subtitle {
|
||||
margin-block-end: 0.4rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin-block: 1.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<VtsSpaceCard
|
||||
:used="sr.physical_utilisation"
|
||||
:total="sr.physical_size"
|
||||
:label="sr.name_label"
|
||||
:total-size-label="t('total-space')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { XenApiSr } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import VtsSpaceCard from '@core/components/space-card/VtsSpaceCard.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<{
|
||||
sr: XenApiSr
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<UiCard class="card-container">
|
||||
<UiCardTitle>
|
||||
<div class="title">
|
||||
{{ t('vdis') }}
|
||||
<UiCounter :value="vdis.length + vdiSnapshots.length" accent="neutral" size="small" variant="primary" />
|
||||
</div>
|
||||
</UiCardTitle>
|
||||
|
||||
<div v-if="vdis.length > 0 || vdiSnapshots.length > 0" class="content">
|
||||
<template v-if="vdis.length > 0">
|
||||
<div class="subsection">
|
||||
<span class="subtitle typo-body-bold-small">{{ t('vdis') }}</span>
|
||||
<UiCounter :value="vdis.length" accent="neutral" size="small" variant="primary" />
|
||||
</div>
|
||||
|
||||
<UiCollapsibleList tag="ul" :total-items="vdis.length">
|
||||
<li v-for="vdi in vdis" :key="vdi.uuid" v-tooltip class="text-ellipsis">
|
||||
<UiLink size="small" :icon="getVdiIcon(getVbdsForVdi(vdi, getVbdByOpaqueRef))">
|
||||
{{ vdi.name_label || t('unknown') }}
|
||||
</UiLink>
|
||||
</li>
|
||||
</UiCollapsibleList>
|
||||
</template>
|
||||
|
||||
<VtsDivider v-if="vdis.length > 0 && vdiSnapshots.length > 0" type="stretch" />
|
||||
|
||||
<template v-if="vdiSnapshots.length > 0">
|
||||
<div class="subsection">
|
||||
<span class="subtitle typo-body-bold-small">{{ t('snapshot-vdis') }}</span>
|
||||
<UiCounter :value="vdiSnapshots.length" accent="neutral" size="small" variant="primary" />
|
||||
</div>
|
||||
|
||||
<UiCollapsibleList tag="ul" :total-items="vdiSnapshots.length">
|
||||
<li v-for="vdiSnapshot in vdiSnapshots" :key="vdiSnapshot.uuid" v-tooltip class="text-ellipsis">
|
||||
<UiLink size="small" icon="object:vdi-snapshot">
|
||||
{{ vdiSnapshot.name_label || t('unknown') }}
|
||||
</UiLink>
|
||||
</li>
|
||||
</UiCollapsibleList>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<VtsStateHero v-else type="no-data" format="card" horizontal size="extra-small">
|
||||
{{ t('no-vdi-attached') }}
|
||||
</VtsStateHero>
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getVdiIcon, getVbdsForVdi } from '@/libs/vdi.ts'
|
||||
import type { XenApiVdi } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import { useVbdStore } from '@/stores/xen-api/vbd.store.ts'
|
||||
import VtsDivider from '@core/components/divider/VtsDivider.vue'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.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 { vTooltip } from '@core/directives/tooltip.directive.ts'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<{
|
||||
vdis: XenApiVdi[]
|
||||
vdiSnapshots: XenApiVdi[]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { getByOpaqueRef: getVbdByOpaqueRef } = useVbdStore().subscribe()
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.card-container {
|
||||
gap: 1.6rem;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
|
||||
.subsection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
|
||||
.subtitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { XenApiPbd } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import { CONNECTION_STATUS } from '@core/types/connection.ts'
|
||||
import { toComputed } from '@core/utils/to-computed.util.ts'
|
||||
import { useArrayEvery, useArrayFilter, useArraySome } from '@vueuse/shared'
|
||||
import { computed, type MaybeRefOrGetter } from 'vue'
|
||||
|
||||
export function usePbdUtils(rawPbds: MaybeRefOrGetter<XenApiPbd[]>) {
|
||||
const pbds = toComputed(rawPbds)
|
||||
|
||||
const predicate = (pbd: XenApiPbd) => !pbd.currently_attached
|
||||
|
||||
const disconnectedPbds = useArrayFilter(pbds, predicate)
|
||||
|
||||
const areAllPbdsDisconnected = useArrayEvery(pbds, predicate)
|
||||
|
||||
const areSomePbdsDisconnected = useArraySome(pbds, predicate)
|
||||
|
||||
const allPbdsConnectionStatus = computed(() => {
|
||||
if (areAllPbdsDisconnected.value) {
|
||||
return CONNECTION_STATUS.DISCONNECTED
|
||||
}
|
||||
|
||||
if (areSomePbdsDisconnected.value) {
|
||||
return CONNECTION_STATUS.PARTIALLY_CONNECTED
|
||||
}
|
||||
|
||||
return CONNECTION_STATUS.CONNECTED
|
||||
})
|
||||
|
||||
return {
|
||||
allPbdsConnectionStatus,
|
||||
areAllPbdsDisconnected,
|
||||
areSomePbdsDisconnected,
|
||||
disconnectedPbds,
|
||||
}
|
||||
}
|
||||
79
@xen-orchestra/lite/src/pages/host/[uuid]/storage.vue
Normal file
79
@xen-orchestra/lite/src/pages/host/[uuid]/storage.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<VtsContentSidePanel class="host-storage-view" :class="{ mobile: uiStore.isSmall }">
|
||||
<UiCard class="container">
|
||||
<StorageRepositoriesTable v-if="pool" :srs :pool :busy="!isReady" :error="hasError" />
|
||||
</UiCard>
|
||||
<StorageRepositorySidePanel v-if="pool" :sr="selectedSr" :pool @close="selectedSr = undefined" />
|
||||
</VtsContentSidePanel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { XenApiHost, XenApiSr } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import StorageRepositorySidePanel from '@/modules/storage-repository/components/list/panel/StorageRepositorySidePanel.vue'
|
||||
import StorageRepositoriesTable from '@/modules/storage-repository/components/list/StorageRepositoriesTable.vue'
|
||||
import { usePageTitleStore } from '@/stores/page-title.store.ts'
|
||||
import { useHostStore } from '@/stores/xen-api/host.store.ts'
|
||||
import { usePbdStore } from '@/stores/xen-api/pbd.store.ts'
|
||||
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
|
||||
import { useSrStore } from '@/stores/xen-api/sr.store.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 { useUiStore } from '@core/stores/ui.store.ts'
|
||||
import { sortByNameLabel } from '@core/utils/sort-by-name-label.util.ts'
|
||||
import { logicAnd } from '@vueuse/math'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
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 { pool } = usePoolStore().subscribe()
|
||||
const uiStore = useUiStore()
|
||||
|
||||
const isReady = logicAnd(areSrsReady, arePbdsReady)
|
||||
|
||||
const host = computed(() => getHostByUuid(route.params.uuid as XenApiHost['uuid']))
|
||||
|
||||
const srs = computed(() => {
|
||||
if (host.value === undefined) {
|
||||
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)
|
||||
})
|
||||
|
||||
const selectedSr = useRouteQuery<XenApiSr | undefined>('id', {
|
||||
toData: id => srs.value.find(sr => sr.uuid === id),
|
||||
toQuery: sr => sr?.uuid ?? '',
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.host-storage-view {
|
||||
.container {
|
||||
height: fit-content;
|
||||
margin: 0.8rem;
|
||||
gap: 4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,54 @@
|
||||
<template>
|
||||
<PageUnderConstruction />
|
||||
<VtsContentSidePanel class="pool-storage-view" :class="{ mobile: uiStore.isSmall }">
|
||||
<UiCard class="container">
|
||||
<StorageRepositoriesTable v-if="pool" :srs="sortedSrs" :pool :busy="!isReady" :error="hasError" />
|
||||
</UiCard>
|
||||
<StorageRepositorySidePanel v-if="pool" :sr="selectedSr" :pool @close="selectedSr = undefined" />
|
||||
</VtsContentSidePanel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PageUnderConstruction from '@/components/PageUnderConstruction.vue'
|
||||
import { usePageTitleStore } from '@/stores/page-title.store'
|
||||
import type { XenApiSr } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import StorageRepositorySidePanel from '@/modules/storage-repository/components/list/panel/StorageRepositorySidePanel.vue'
|
||||
import StorageRepositoriesTable from '@/modules/storage-repository/components/list/StorageRepositoriesTable.vue'
|
||||
import { usePageTitleStore } from '@/stores/page-title.store.ts'
|
||||
import { usePbdStore } from '@/stores/xen-api/pbd.store.ts'
|
||||
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
|
||||
import { useSrStore } from '@/stores/xen-api/sr.store.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 { useUiStore } from '@core/stores/ui.store.ts'
|
||||
import { sortByNameLabel } from '@core/utils/sort-by-name-label.util.ts'
|
||||
import { logicAnd } from '@vueuse/math'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
usePageTitleStore().setTitle(useI18n().t('storage'))
|
||||
const { t } = useI18n()
|
||||
|
||||
usePageTitleStore().setTitle(t('storage'))
|
||||
|
||||
const { pool } = usePoolStore().subscribe()
|
||||
const { records: srs, isReady: areSrsReady, hasError } = useSrStore().subscribe()
|
||||
const { isReady: arePbdsReady } = usePbdStore().subscribe()
|
||||
const uiStore = useUiStore()
|
||||
|
||||
const isReady = logicAnd(areSrsReady, arePbdsReady)
|
||||
|
||||
const sortedSrs = computed(() => [...srs.value].sort(sortByNameLabel))
|
||||
|
||||
const selectedSr = useRouteQuery<XenApiSr | undefined>('id', {
|
||||
toData: id => sortedSrs.value.find(sr => sr.uuid === id),
|
||||
toQuery: sr => sr?.uuid ?? '',
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.pool-storage-view {
|
||||
.container {
|
||||
height: fit-content;
|
||||
margin: 0.8rem;
|
||||
gap: 4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<PageUnderConstruction />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PageUnderConstruction from '@/components/PageUnderConstruction.vue'
|
||||
import { usePageTitleStore } from '@/stores/page-title.store'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
usePageTitleStore().setTitle(useI18n().t('storage'))
|
||||
</script>
|
||||
30
@xen-orchestra/lite/src/route-map.d.ts
vendored
30
@xen-orchestra/lite/src/route-map.d.ts
vendored
@@ -52,6 +52,7 @@ declare module 'vue-router/auto-routes' {
|
||||
| '/host/[uuid]/console'
|
||||
| '/host/[uuid]/dashboard'
|
||||
| '/host/[uuid]/network'
|
||||
| '/host/[uuid]/storage'
|
||||
| '/host/[uuid]/system'
|
||||
| '/host/[uuid]/tasks'
|
||||
| '/host/[uuid]/vms'
|
||||
@@ -77,6 +78,13 @@ declare module 'vue-router/auto-routes' {
|
||||
{ uuid: ParamValue<false> },
|
||||
| never
|
||||
>,
|
||||
'/host/[uuid]/storage': RouteRecordInfo<
|
||||
'/host/[uuid]/storage',
|
||||
'/host/:uuid/storage',
|
||||
{ uuid: ParamValue<true> },
|
||||
{ uuid: ParamValue<false> },
|
||||
| never
|
||||
>,
|
||||
'/host/[uuid]/system': RouteRecordInfo<
|
||||
'/host/[uuid]/system',
|
||||
'/host/:uuid/system',
|
||||
@@ -928,7 +936,6 @@ declare module 'vue-router/auto-routes' {
|
||||
| '/vm/[uuid]/dashboard'
|
||||
| '/vm/[uuid]/network'
|
||||
| '/vm/[uuid]/stats'
|
||||
| '/vm/[uuid]/storage'
|
||||
| '/vm/[uuid]/system'
|
||||
| '/vm/[uuid]/tasks'
|
||||
>,
|
||||
@@ -967,13 +974,6 @@ declare module 'vue-router/auto-routes' {
|
||||
{ uuid: ParamValue<false> },
|
||||
| never
|
||||
>,
|
||||
'/vm/[uuid]/storage': RouteRecordInfo<
|
||||
'/vm/[uuid]/storage',
|
||||
'/vm/:uuid/storage',
|
||||
{ uuid: ParamValue<true> },
|
||||
{ uuid: ParamValue<false> },
|
||||
| never
|
||||
>,
|
||||
'/vm/[uuid]/system': RouteRecordInfo<
|
||||
'/vm/[uuid]/system',
|
||||
'/vm/:uuid/system',
|
||||
@@ -1033,6 +1033,7 @@ declare module 'vue-router/auto-routes' {
|
||||
| '/host/[uuid]/console'
|
||||
| '/host/[uuid]/dashboard'
|
||||
| '/host/[uuid]/network'
|
||||
| '/host/[uuid]/storage'
|
||||
| '/host/[uuid]/system'
|
||||
| '/host/[uuid]/tasks'
|
||||
| '/host/[uuid]/vms'
|
||||
@@ -1057,6 +1058,12 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/host/[uuid]/storage.vue': {
|
||||
routes:
|
||||
| '/host/[uuid]/storage'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/host/[uuid]/system.vue': {
|
||||
routes:
|
||||
| '/host/[uuid]/system'
|
||||
@@ -1788,7 +1795,6 @@ declare module 'vue-router/auto-routes' {
|
||||
| '/vm/[uuid]/dashboard'
|
||||
| '/vm/[uuid]/network'
|
||||
| '/vm/[uuid]/stats'
|
||||
| '/vm/[uuid]/storage'
|
||||
| '/vm/[uuid]/system'
|
||||
| '/vm/[uuid]/tasks'
|
||||
views:
|
||||
@@ -1824,12 +1830,6 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/vm/[uuid]/storage.vue': {
|
||||
routes:
|
||||
| '/vm/[uuid]/storage'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/vm/[uuid]/system.vue': {
|
||||
routes:
|
||||
| '/vm/[uuid]/system'
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import { createXapiStoreConfig } from '@/stores/xen-api/create-xapi-store-config'
|
||||
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
|
||||
import type { XenApiHost, XenApiSr } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import { createXapiStoreConfig } from '@/stores/xen-api/create-xapi-store-config.ts'
|
||||
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util.ts'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const usePbdStore = defineStore('xen-api-pbd', () => {
|
||||
const config = createXapiStoreConfig('pbd')
|
||||
const { context: baseContext, ...configRest } = createXapiStoreConfig('pbd')
|
||||
|
||||
return createSubscribableStoreContext(config, {})
|
||||
const getPbdsForHost = (hostRef: XenApiHost['$ref']) => baseContext.records.value.filter(pbd => pbd.host === hostRef)
|
||||
|
||||
const getPbdsForSr = (srRef: XenApiSr['$ref']) => baseContext.records.value.filter(pbd => pbd.SR === srRef)
|
||||
|
||||
const context = {
|
||||
...baseContext,
|
||||
getPbdsForHost,
|
||||
getPbdsForSr,
|
||||
}
|
||||
|
||||
return createSubscribableStoreContext({ context, ...configRest }, {})
|
||||
})
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import type { XenApiSr, XenApiVdi } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import { createXapiStoreConfig } from '@/stores/xen-api/create-xapi-store-config'
|
||||
import { useVdiStore } from '@/stores/xen-api/vdi.store'
|
||||
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
|
||||
import { getAllocationStrategy as getAllocationStrategyFromSrType } from '@/libs/sr.ts'
|
||||
import type { 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'
|
||||
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util.ts'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
export const useSrStore = defineStore('xen-api-sr', () => {
|
||||
const deps = {
|
||||
vdiStore: useVdiStore(),
|
||||
pbdStore: usePbdStore(),
|
||||
}
|
||||
|
||||
const vdiContext = deps.vdiStore.getContext()
|
||||
|
||||
const pbdContext = deps.pbdStore.getContext()
|
||||
|
||||
const { context: baseContext, ...configRest } = createXapiStoreConfig('sr')
|
||||
|
||||
const srs = computed(() => baseContext.records.value)
|
||||
@@ -31,6 +36,17 @@ 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 isDefaultSr = (sr: XenApiSr, pool: XenApiPool) =>
|
||||
pool.default_SR !== 'OpaqueRef:NULL' && pool.default_SR === sr.$ref
|
||||
|
||||
const isHaSr = (sr: XenApiSr, pool: XenApiPool) =>
|
||||
pool.ha_statefiles.some(vdiRef => vdiContext.getByOpaqueRef(vdiRef)?.SR === sr.$ref)
|
||||
|
||||
const getAllocationStrategy = (sr: XenApiSr) => {
|
||||
const firstPbd = pbdContext.getByOpaqueRef(sr.PBDs[0])
|
||||
return getAllocationStrategyFromSrType(sr.type, firstPbd?.device_config.provisioning)
|
||||
}
|
||||
|
||||
const vdiIsosBySrName = computed(() => {
|
||||
const groupedVdis: Record<string, XenApiVdi[]> = {}
|
||||
|
||||
@@ -51,6 +67,9 @@ export const useSrStore = defineStore('xen-api-sr', () => {
|
||||
|
||||
const context = {
|
||||
...baseContext,
|
||||
getAllocationStrategy,
|
||||
isDefaultSr,
|
||||
isHaSr,
|
||||
vdiIsosBySrName,
|
||||
}
|
||||
|
||||
|
||||
@@ -79,15 +79,7 @@ const panelSignature = computed(() => getSrPbdsSignature(sr, scope))
|
||||
const pbds = computed(() => (sr !== undefined ? pbdsBySr.value.get(sr.id) : undefined) ?? [])
|
||||
|
||||
const hosts = computed(() =>
|
||||
pbds.value.reduce<FrontXoHost[]>((acc, pbd) => {
|
||||
const host = getHostById(pbd.host)
|
||||
|
||||
if (host !== undefined) {
|
||||
acc.push(host)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
pbds.value.map(pbd => getHostById(pbd.host)).filter((host): host is FrontXoHost => host !== undefined)
|
||||
)
|
||||
|
||||
const customFields = computed(() => {
|
||||
|
||||
@@ -42,9 +42,9 @@
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('provisioning') }}</template>
|
||||
<template #value>{{ provisioning }}</template>
|
||||
<template #value>{{ allocationStrategy }}</template>
|
||||
<template #addons>
|
||||
<VtsCopyButton :value="provisioning" />
|
||||
<VtsCopyButton :value="allocationStrategy" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
@@ -92,7 +92,7 @@ const { srConnectionStatus, srStatusIcon } = useXoSrUtils(
|
||||
|
||||
const isSrSharedI18nValue = computed(() => (sr.shared ? t('shared') : t('local')))
|
||||
|
||||
const provisioning = computed(() => {
|
||||
const allocationStrategy = computed(() => {
|
||||
return sr.allocationStrategy ?? t('unknown')
|
||||
})
|
||||
|
||||
|
||||
@@ -8,43 +8,49 @@
|
||||
</UiCardTitle>
|
||||
|
||||
<div v-if="vdis.length > 0 || vdiSnapshots.length > 0" class="content">
|
||||
<div v-if="vdis.length > 0" class="subsection">
|
||||
<span class="subtitle typo-body-bold-small">{{ t('vdis') }}</span>
|
||||
<UiCounter :value="vdis.length" accent="neutral" size="small" variant="primary" />
|
||||
</div>
|
||||
<UiCollapsibleList tag="ul" :total-items="vdis.length">
|
||||
<li v-for="vdi in vdis" :key="vdi.id" v-tooltip class="text-ellipsis">
|
||||
<UiLink
|
||||
:to="{ name: '/vdi/[id]/general', params: { id: vdi.id }, query: { from: VDI_PAGE_CONTEXT.SR } }"
|
||||
size="small"
|
||||
:icon="getVdiIcon(getVbdsByIds(vdi.$VBDs))"
|
||||
>
|
||||
{{ vdi.name_label || t('unknown') }}
|
||||
</UiLink>
|
||||
</li>
|
||||
</UiCollapsibleList>
|
||||
<template v-if="vdis.length > 0">
|
||||
<div class="subsection">
|
||||
<span class="subtitle typo-body-bold-small">{{ t('vdis') }}</span>
|
||||
<UiCounter :value="vdis.length" accent="neutral" size="small" variant="primary" />
|
||||
</div>
|
||||
|
||||
<UiCollapsibleList tag="ul" :total-items="vdis.length">
|
||||
<li v-for="vdi in vdis" :key="vdi.id" v-tooltip class="text-ellipsis">
|
||||
<UiLink
|
||||
:to="{ name: '/vdi/[id]/general', params: { id: vdi.id }, query: { from: VDI_PAGE_CONTEXT.SR } }"
|
||||
size="small"
|
||||
:icon="getVdiIcon(getVbdsByIds(vdi.$VBDs))"
|
||||
>
|
||||
{{ vdi.name_label || t('unknown') }}
|
||||
</UiLink>
|
||||
</li>
|
||||
</UiCollapsibleList>
|
||||
</template>
|
||||
|
||||
<VtsDivider v-if="vdis.length > 0 && vdiSnapshots.length > 0" type="stretch" />
|
||||
|
||||
<div v-if="vdiSnapshots.length > 0" class="subsection">
|
||||
<span class="subtitle typo-body-bold-small">{{ t('snapshot-vdis') }}</span>
|
||||
<UiCounter :value="vdiSnapshots.length" accent="neutral" size="small" variant="primary" />
|
||||
</div>
|
||||
<UiCollapsibleList tag="ul" :total-items="vdiSnapshots.length">
|
||||
<li v-for="vdiSnapshot in vdiSnapshots" :key="vdiSnapshot.id" v-tooltip class="text-ellipsis">
|
||||
<UiLink
|
||||
:to="{
|
||||
name: '/vdi/[id]/general',
|
||||
params: { id: vdiSnapshot.id },
|
||||
query: { from: VDI_PAGE_CONTEXT.VDI_SNAPSHOT },
|
||||
}"
|
||||
size="small"
|
||||
icon="object:vdi-snapshot"
|
||||
>
|
||||
{{ vdiSnapshot.name_label || t('unknown') }}
|
||||
</UiLink>
|
||||
</li>
|
||||
</UiCollapsibleList>
|
||||
<template v-if="vdiSnapshots.length > 0">
|
||||
<div class="subsection">
|
||||
<span class="subtitle typo-body-bold-small">{{ t('snapshot-vdis') }}</span>
|
||||
<UiCounter :value="vdiSnapshots.length" accent="neutral" size="small" variant="primary" />
|
||||
</div>
|
||||
|
||||
<UiCollapsibleList tag="ul" :total-items="vdiSnapshots.length">
|
||||
<li v-for="vdiSnapshot in vdiSnapshots" :key="vdiSnapshot.id" v-tooltip class="text-ellipsis">
|
||||
<UiLink
|
||||
:to="{
|
||||
name: '/vdi/[id]/general',
|
||||
params: { id: vdiSnapshot.id },
|
||||
query: { from: VDI_PAGE_CONTEXT.VDI_SNAPSHOT },
|
||||
}"
|
||||
size="small"
|
||||
icon="object:vdi-snapshot"
|
||||
>
|
||||
{{ vdiSnapshot.name_label || t('unknown') }}
|
||||
</UiLink>
|
||||
</li>
|
||||
</UiCollapsibleList>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<VtsStateHero v-else type="no-data" format="card" horizontal size="extra-small">
|
||||
|
||||
Reference in New Issue
Block a user