feat(xo-6/treeview): introduce site level in treeview (#8694)

This commit is contained in:
OlivierFL
2025-06-17 08:59:41 +02:00
committed by GitHub
parent 486034d32b
commit beeef40eda
14 changed files with 222 additions and 75 deletions

View File

@@ -217,7 +217,16 @@ module.exports = {
'@intlify/vue-i18n/no-raw-text': [
'error',
{
ignoreText: ['Xen Orchestra Lite', 'Xen Orchestra', 'XO Lite', 'XCP-ng', 'XO 5', '404', '⚠️'],
ignoreText: [
'Xen Orchestra Lite',
'Xen Orchestra',
'Xen Orchestra Appliance',
'XO Lite',
'XCP-ng',
'XO 5',
'404',
'⚠️',
],
},
],
'@intlify/vue-i18n/no-v-html': 'error',

View File

@@ -0,0 +1,35 @@
<template>
<UiHeadBar :icon="faSatellite">
Xen Orchestra Appliance
<template #actions>
<UiButton :left-icon="faPlus" variant="secondary" accent="brand" size="medium" @click="goToNewVm()">
{{ $t('new-vm') }}
</UiButton>
</template>
</UiHeadBar>
<TabList>
<RouterLink v-slot="{ isExactActive, href }" to="/" custom>
<TabItem :active="isExactActive" :href tag="a">
{{ $t('dashboard') }}
</TabItem>
</RouterLink>
<TabItem disabled>{{ $t('backups') }}</TabItem>
<TabItem disabled>{{ $t('tasks') }}</TabItem>
<TabItem disabled>{{ $t('pools') }}</TabItem>
<TabItem disabled>{{ $t('hosts') }}</TabItem>
<TabItem disabled>{{ $t('vms') }}</TabItem>
</TabList>
</template>
<script lang="ts" setup>
import TabItem from '@core/components/tab/TabItem.vue'
import TabList from '@core/components/tab/TabList.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiHeadBar from '@core/components/ui/head-bar/UiHeadBar.vue'
import { faPlus, faSatellite } from '@fortawesome/free-solid-svg-icons'
import { useRouter } from 'vue-router'
const router = useRouter()
const goToNewVm = () => router.push({ name: '/vm/new' })
</script>

View File

@@ -1,5 +1,5 @@
<template>
<VtsTreeList class="pool-tree-list">
<VtsTreeList>
<PoolTreeItem v-for="branch in branches" :key="branch.id" :branch />
</VtsTreeList>
</template>
@@ -13,11 +13,3 @@ defineProps<{
branches: PoolBranch[]
}>()
</script>
<style lang="postcss" scoped>
.pool-tree-list {
background-color: var(--color-neutral-background-primary);
padding: 0.8rem;
min-height: 100%;
}
</style>

View File

@@ -0,0 +1,42 @@
<template>
<VtsTreeItem :expanded="branch.isExpanded">
<UiTreeItemLabel :icon="faSatellite" route="/" @toggle="branch.toggleExpand()">
{{ branch.data.name_label }}
<template #addons>
<UiCounter
v-tooltip="t('running-vm', runningVmsCount)"
:value="runningVmsCount"
accent="brand"
variant="secondary"
size="small"
/>
</template>
</UiTreeItemLabel>
<template v-if="branch.hasChildren" #sublist>
<PoolTreeList :branches="branch.children" />
</template>
</VtsTreeItem>
</template>
<script lang="ts" setup>
import PoolTreeList from '@/components/tree/PoolTreeList.vue'
import { useVmStore } from '@/stores/xo-rest-api/vm.store.ts'
import type { SiteBranch } from '@/types/tree.type'
import VtsTreeItem from '@core/components/tree/VtsTreeItem.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import UiTreeItemLabel from '@core/components/ui/tree-item-label/UiTreeItemLabel.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import { faSatellite } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
defineProps<{
branch: SiteBranch
}>()
const { t } = useI18n()
const { runningVms } = useVmStore().subscribe()
const runningVmsCount = computed(() => runningVms.value.length)
</script>

View File

@@ -0,0 +1,23 @@
<template>
<VtsTreeList class="site-tree-list">
<SiteTreeItem v-for="branch in branches" :key="branch.id" :branch />
</VtsTreeList>
</template>
<script lang="ts" setup>
import SiteTreeItem from '@/components/tree/SiteTreeItem.vue'
import type { SiteBranch } from '@/types/tree.type'
import VtsTreeList from '@core/components/tree/VtsTreeList.vue'
defineProps<{
branches: SiteBranch[]
}>()
</script>
<style lang="postcss" scoped>
.site-tree-list {
background-color: var(--color-neutral-background-primary);
padding: 0.8rem;
min-height: 100%;
}
</style>

View File

@@ -1,46 +1,63 @@
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { usePoolStore } from '@/stores/xo-rest-api/pool.store'
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
import type { XoSite } from '@/types/xo/site.type.ts'
import { defineTree } from '@core/composables/tree/define-tree'
import { useTreeFilter } from '@core/composables/tree-filter.composable'
import { useTree } from '@core/composables/tree.composable'
import { logicAnd } from '@vueuse/math'
import { computed } from 'vue'
import { computed, useId } from 'vue'
export function usePoolTree() {
export function useSiteTree() {
const { records: pools, isReady: isPoolReady } = usePoolStore().subscribe()
const { hostsByPool, isReady: isHostReady } = useHostStore().subscribe()
const { vmsByHost, hostLessVmsByPool, isReady: isVmReady } = useVmStore().subscribe()
const { filter, predicate } = useTreeFilter()
const site: XoSite = {
id: useId(),
type: 'site',
name_label: 'Xen Orchestra Appliance',
}
const sites = computed(() => [site])
const isReady = logicAnd(isPoolReady, isHostReady, isVmReady)
const definitions = computed(() =>
defineTree(
pools.value,
sites.value,
{
getLabel: 'name_label',
predicate,
},
pool => [
() => [
...defineTree(
hostsByPool.value.get(pool.id) ?? [],
pools.value,
{
getLabel: 'name_label',
predicate,
discriminator: 'host',
},
host =>
defineTree(vmsByHost.value.get(host.id) ?? [], {
pool => [
...defineTree(
hostsByPool.value.get(pool.id) ?? [],
{
getLabel: 'name_label',
predicate,
discriminator: 'host',
},
host =>
defineTree(vmsByHost.value.get(host.id) ?? [], {
getLabel: 'name_label',
predicate,
})
),
...defineTree(hostLessVmsByPool.value.get(pool.id) ?? [], {
getLabel: 'name_label',
predicate,
})
discriminator: 'vm',
}),
]
),
...defineTree(hostLessVmsByPool.value.get(pool.id) ?? [], {
getLabel: 'name_label',
predicate,
discriminator: 'vm',
}),
]
)
)
@@ -49,7 +66,7 @@ export function usePoolTree() {
return {
isReady,
pools: nodes,
sites: nodes,
filter,
}
}

View File

@@ -25,8 +25,8 @@
<VtsTreeList v-if="!isReady">
<VtsTreeLoadingItem v-for="i in 5" :key="i" :icon="faCity" />
</VtsTreeList>
<NoResults v-else-if="pools.length === 0" />
<PoolTreeList v-else :branches="pools" />
<NoResults v-else-if="sites.length === 0" />
<SiteTreeList v-else :branches="sites" />
</template>
<template #content>
<slot />
@@ -40,8 +40,8 @@ import LogoTextOnly from '@/components/LogoTextOnly.vue'
import NoResults from '@/components/NoResults.vue'
import SidebarSearch from '@/components/SidebarSearch.vue'
import QuickTaskButton from '@/components/task/QuickTaskButton.vue'
import PoolTreeList from '@/components/tree/PoolTreeList.vue'
import { usePoolTree } from '@/composables/pool-tree.composable'
import SiteTreeList from '@/components/tree/SiteTreeList.vue'
import { useSiteTree } from '@/composables/pool-tree.composable'
import VtsTreeList from '@core/components/tree/VtsTreeList.vue'
import VtsTreeLoadingItem from '@core/components/tree/VtsTreeLoadingItem.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
@@ -50,9 +50,13 @@ import { useUiStore } from '@core/stores/ui.store'
import { openUrl } from '@core/utils/open-url.utils'
import { faArrowUpRightFromSquare, faCity } from '@fortawesome/free-solid-svg-icons'
defineSlots<{
default(): any
}>()
const uiStore = useUiStore()
const { pools, isReady, filter } = usePoolTree()
const { sites, isReady, filter } = useSiteTree()
</script>
<style lang="postcss" scoped>

View File

@@ -0,0 +1,15 @@
<template>
<VtsLoadingHero v-if="!isReady" type="page" />
<RouterView v-slot="{ Component }">
<SiteHeader />
<component :is="Component" />
</RouterView>
</template>
<script lang="ts" setup>
import SiteHeader from '@/components/site/SiteHeader.vue'
import { usePoolStore } from '@/stores/xo-rest-api/pool.store.ts'
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
const { isReady } = usePoolStore().subscribe()
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div class="site-dashboard">
<div class="site-dashboard" :class="{ mobile: uiStore.isMobile }">
<PoolsStatus class="pools-status" />
<HostsStatus class="hosts-status" />
<VmsStatus class="vms-status" />
@@ -20,6 +20,9 @@ import PoolsStatus from '@/components/site/dashboard/PoolsStatus.vue'
import Repositories from '@/components/site/dashboard/Repositories.vue'
import ResourcesOverview from '@/components/site/dashboard/ResourcesOverview.vue'
import VmsStatus from '@/components/site/dashboard/VmsStatus.vue'
import { useUiStore } from '@core/stores/ui.store.ts'
const uiStore = useUiStore()
</script>
<style lang="postcss" scoped>
@@ -33,42 +36,8 @@ import VmsStatus from '@/components/site/dashboard/VmsStatus.vue'
'backups backups backups backup-issues backup-issues backup-issues backup-issues backup-issues'
'repositories repositories repositories repositories repositories repositories repositories repositories'
'alarms alarms alarms alarms alarms alarms patches patches';
}
.pools-status {
grid-area: pools-status;
}
.hosts-status {
grid-area: hosts-status;
}
.vms-status {
grid-area: vms-status;
}
.resources-overview {
grid-area: resources-overview;
}
.backups {
grid-area: backups;
}
.backup-issues {
grid-area: backup-issues;
}
.repositories {
grid-area: repositories;
}
.patches {
grid-area: patches;
}
@media (--mobile) {
.site-dashboard {
&.mobile {
grid-template-columns: 1fr;
grid-template-areas:
'pools-status'
@@ -80,5 +49,37 @@ import VmsStatus from '@/components/site/dashboard/VmsStatus.vue'
'repositories'
'patches';
}
.pools-status {
grid-area: pools-status;
}
.hosts-status {
grid-area: hosts-status;
}
.vms-status {
grid-area: vms-status;
}
.resources-overview {
grid-area: resources-overview;
}
.backups {
grid-area: backups;
}
.backup-issues {
grid-area: backup-issues;
}
.repositories {
grid-area: repositories;
}
.patches {
grid-area: patches;
}
}
</style>

View File

@@ -370,7 +370,7 @@
<UiToaster v-if="isOpen" accent="danger" @close="isOpen = false">{{ errorMessage }}</UiToaster>
<!-- ACTIONS -->
<div class="footer">
<UiButton variant="secondary" accent="brand" size="medium" @click="redirectToHome()">
<UiButton variant="secondary" accent="brand" size="medium" @click="redirectToPool(vmState.pool.id)">
{{ t('cancel') }}
</UiButton>
<UiButton
@@ -403,6 +403,7 @@ import { useVifStore } from '@/stores/xo-rest-api/vif.store'
import { useVmTemplateStore } from '@/stores/xo-rest-api/vm-template.store'
import type { XoNetwork } from '@/types/xo/network.type.ts'
import type { Vdi, Vif, VifToSend, VmState } from '@/types/xo/new-vm.type'
import type { XoPool } from '@/types/xo/pool.type.ts'
import type { XoVdi } from '@/types/xo/vdi.type.ts'
import type { XoVmTemplate } from '@/types/xo/vm-template.type'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
@@ -758,8 +759,9 @@ const onTemplateChange = () => {
})
}
const redirectToHome = () => {
router.push({ name: '/' })
// TODO: when refactoring the component, remove the param and sync with the pool id in the route
const redirectToPool = (poolId: XoPool['id']) => {
router.push({ name: '/pool/:id', params: { id: poolId } })
}
function getExistingVdisDiff(vdi1: Vdi, vdi2: Vdi) {
@@ -903,7 +905,7 @@ const createNewVM = async () => {
}
await createVM(vmData.value, vmState.pool.id)
redirectToHome()
redirectToPool(vmState.pool.id)
} catch (error) {
isOpen.value = true

View File

@@ -1,5 +1,6 @@
import type { XoHost } from '@/types/xo/host.type'
import type { XoPool } from '@/types/xo/pool.type'
import type { XoSite } from '@/types/xo/site.type.ts'
import type { XoVm } from '@/types/xo/vm.type'
import type { Branch } from '@core/composables/tree/branch'
import type { Leaf } from '@core/composables/tree/leaf'
@@ -9,3 +10,5 @@ export type VmLeaf = Leaf<XoVm, 'vm'>
export type HostBranch = Branch<XoHost, VmLeaf, 'host'>
export type PoolBranch = Branch<XoPool, HostBranch | VmLeaf, 'pool'>
export type SiteBranch = Branch<XoSite, PoolBranch, 'site'>

View File

@@ -0,0 +1,5 @@
export type XoSite = {
id: string
type: 'site'
name_label: string
}

View File

@@ -18,7 +18,8 @@ declare module 'vue-router/auto-routes' {
* Route name map generated by unplugin-vue-router
*/
export interface RouteNamedMap {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/(site)': RouteRecordInfo<'/(site)', '/', Record<never, never>, Record<never, never>>,
'/(site)/(dashboard)': RouteRecordInfo<'/(site)/(dashboard)', '/', Record<never, never>, Record<never, never>>,
'/[...path]': RouteRecordInfo<'/[...path]', '/:path(.*)', { path: ParamValue<true> }, { path: ParamValue<false> }>,
'/dev/': RouteRecordInfo<'/dev/', '/dev', Record<never, never>, Record<never, never>>,
'/dev/colors': RouteRecordInfo<'/dev/colors', '/dev/colors', Record<never, never>, Record<never, never>>,

View File

@@ -12,13 +12,11 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- **XO 6:**
- [Pool/system] Display pool information in pool/system tab (PR [#8581](https://github.com/vatesfr/xen-orchestra/pull/8581))
- [Host/Dashboard] Update RAM usage components wordings and update CPU provisioning logic (PR [#8648](https://github.com/vatesfr/xen-orchestra/pull/8648))
- [Site/Dashboard] Update BackupIssues and VtsBackupState components to display data in table (PR [#8674](https://github.com/vatesfr/xen-orchestra/pull/8674)
- [Site] Add "Site" level in treeview and add "Site" header and tabs (PR [#8694](https://github.com/vatesfr/xen-orchestra/pull/8694))
- **Migrated REST API endpoints**
- `/rest/v0/pools/<pool-id>/actions/emergency_shutdown` (PR [#8653](https://github.com/vatesfr/xen-orchestra/pull/8653))
- `/rest/v0/pools/<pool-id>/actions/rolling_reboot` (PR [#8653](https://github.com/vatesfr/xen-orchestra/pull/8653))
- `/rest/v0/pools/<pool-id>/actions/rolling_update` (PR [#8653](https://github.com/vatesfr/xen-orchestra/pull/8653))