mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-10 22:14:48 -05:00
feat(xo-6/treeview): introduce site level in treeview (#8694)
This commit is contained in:
11
.eslintrc.js
11
.eslintrc.js
@@ -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',
|
||||
|
||||
35
@xen-orchestra/web/src/components/site/SiteHeader.vue
Normal file
35
@xen-orchestra/web/src/components/site/SiteHeader.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
42
@xen-orchestra/web/src/components/tree/SiteTreeItem.vue
Normal file
42
@xen-orchestra/web/src/components/tree/SiteTreeItem.vue
Normal 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>
|
||||
23
@xen-orchestra/web/src/components/tree/SiteTreeList.vue
Normal file
23
@xen-orchestra/web/src/components/tree/SiteTreeList.vue
Normal 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>
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
15
@xen-orchestra/web/src/pages/(site).vue
Normal file
15
@xen-orchestra/web/src/pages/(site).vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
5
@xen-orchestra/web/src/types/xo/site.type.ts
Normal file
5
@xen-orchestra/web/src/types/xo/site.type.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type XoSite = {
|
||||
id: string
|
||||
type: 'site'
|
||||
name_label: string
|
||||
}
|
||||
3
@xen-orchestra/web/typed-router.d.ts
vendored
3
@xen-orchestra/web/typed-router.d.ts
vendored
@@ -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>>,
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user