Skip to content

Commit

Permalink
feat(xo-6/pool): add networks view
Browse files Browse the repository at this point in the history
  • Loading branch information
OlivierFL committed Jan 20, 2025
1 parent bfe735d commit 78092ea
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 77 deletions.
36 changes: 36 additions & 0 deletions @xen-orchestra/web-core/lib/components/table/VtsDataTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<template>
<div class="table-container">
<VtsLoadingHero :disabled="isReady" type="table">
<VtsTable class="table" vertical-border>
<thead>
<slot name="thead" />
</thead>
<tbody>
<slot name="tbody" />
</tbody>
</VtsTable>
</VtsLoadingHero>
<VtsErrorNoDataHero v-if="hasError" type="table" />
<VtsStateHero v-if="noDataMessage" type="table" image="no-data">
{{ noDataMessage }}
</VtsStateHero>
</div>
</template>

<script setup lang="ts">
import VtsErrorNoDataHero from '@core/components/state-hero/VtsErrorNoDataHero.vue'
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
defineProps<{
isReady?: boolean
hasError?: boolean
noDataMessage?: string
}>()
defineSlots<{
thead(): any
tbody(): any
}>()
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
const { disabled, icon } = defineProps<{
disabled: boolean
icon: IconDefinition
disabled?: boolean
}>()
</script>

Expand Down
2 changes: 1 addition & 1 deletion @xen-orchestra/web-core/lib/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"device": "Appareil",
"disabled": "Désactivé",
"disconnected": "Déconnecté",
"disconnected-from-physical-device": "Déconnécté de l'appareil physique",
"disconnected-from-physical-device": "Déconnecté de l'appareil physique",
"documentation-name": "Documentation {name}",
"edit": "Modifier",
"error-no-data": "Erreur, impossible de collecter les données.",
Expand Down
7 changes: 7 additions & 0 deletions @xen-orchestra/web/src/components/pif/PifRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,27 @@ import { computed } from 'vue'
const { pif } = defineProps<{
pif: XoPif
}>()
const { records: hosts } = useHostStore().subscribe()
const status = computed<ConnectionStatus>(() => {
if (pif.carrier && pif.attached) {
return 'connected'
}
if (pif.carrier || pif.attached) {
return 'disconnected-from-physical-device'
}
return 'disconnected'
})
const host = computed(() => hosts.value.find(host => host.id === pif.$host))
const hostState = computed(() => {
return host.value?.power_state ? 'running' : 'disabled'
})
// TODO: Select the right row in network table, wait for the PR #8198 to implementation
// const redirectToHostNetwork = (pif: XoPif) => {
// router.push({ name: '/host/[id]/network', params: { id: pif.$host }, query: { pif: pif.id } })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@

<script setup lang="ts">
import { useNetworkStore } from '@/stores/xo-rest-api/network.store'
import type { XoNetwork } from '@/types/xo/network.type'
import type { XoPool } from '@/types/xo/pool.type'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsErrorNoDataHero from '@core/components/state-hero/VtsErrorNoDataHero.vue'
Expand Down Expand Up @@ -148,8 +147,11 @@ const pagination = ref<PaginationPayload>({
})
const { t } = useI18n()
const { networksWithoutPifs, isReady, hasError } = useNetworkStore().subscribe()
const networksByPool = computed(() => networksWithoutPifs.value.filter(network => network.$pool === pool.id))
const selectedNetworkId = useRouteQuery('id')
const findPageById = () => {
Expand All @@ -169,12 +171,15 @@ const searchQuery = ref('')
const filteredNetworks = computed(() => {
let filtered = networksByPool.value
if (!searchQuery.value) {
return networksByPool.value.slice(pagination.value.startIndex - 1, pagination.value.endIndex)
}
filtered = networksByPool.value.filter(network =>
Object.values(network).some(value => String(value).toLowerCase().includes(searchQuery.value.toLowerCase()))
)
return filtered.slice(pagination.value.startIndex - 1, pagination.value.endIndex)
})
Expand All @@ -194,12 +199,12 @@ const { visibleColumns, rows } = useTable('networks', filteredNetworks, {
rowId: record => record.id,
columns: define => [
define('checkbox', () => '', { label: '', isHideable: false }),
define('name_label', (record: XoNetwork) => record.name_label, { label: t('name') }),
define('name_description', (record: XoNetwork) => record.name_description, {
define('name_label', record => record.name_label, { label: t('name') }),
define('name_description', record => record.name_description, {
label: t('description'),
}),
define('MTU', { label: 'MTU' }),
define('default_locking_mode', (record: XoNetwork) => getLockingMode(record.defaultIsLocked), {
define('default_locking_mode', record => getLockingMode(record.defaultIsLocked), {
label: t('locking-mode-default'),
}),
define('more', () => '', { label: '', isHideable: false }),
Expand Down
17 changes: 0 additions & 17 deletions @xen-orchestra/web/src/components/pool/PoolNetworkPifStatus.vue

This file was deleted.

127 changes: 73 additions & 54 deletions @xen-orchestra/web/src/components/pool/PoolNetworksTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,53 +35,54 @@
</div>
</div>
<div class="table-container">
<VtsLoadingHero :disabled="isReady" type="table">
<VtsTable class="table" vertical-border>
<thead>
<tr>
<template v-for="column of visibleColumns" :key="column.id">
<th v-if="column.id === 'checkbox'" class="checkbox">
<UiCheckbox :v-model="areAllSelected" accent="info" @update:model-value="toggleSelect" />
</th>
<ColumnTitle v-else id="networks" :header-class="`col-${column.id}`" :icon="getHeaderIcon(column.id)">
{{ column.label }}
</ColumnTitle>
</template>
</tr>
</thead>
<tbody>
<tr
v-for="row of rows"
:key="row.id"
:class="{ selected: selectedNetworkId === row.id }"
@click="selectedNetworkId = row.id"
>
<td v-for="column of row.visibleColumns" :key="column.id" class="typo p2-regular">
<div>
<UiCheckbox v-if="column.id === 'checkbox'" v-model="selected" accent="info" :value="row.id" />
</div>
<div v-if="column.id === 'status'" class="status">
<PifStatus :network="row.value" />
</div>
<div v-else v-tooltip="{ placement: 'bottom-end' }" class="text-ellipsis">
{{ column.value }}
</div>
</td>
</tr>
</tbody>
</VtsTable>
</VtsLoadingHero>
<VtsErrorNoDataHero v-if="hasError" type="table" />
<VtsDataTable
:is-ready
:has-error
:no-data-message="networksByPool.length === 0 ? $t('no-network-detected') : undefined"
>
<template #thead>
<tr>
<template v-for="column of visibleColumns" :key="column.id">
<th v-if="column.id === 'checkbox'" class="checkbox">
<UiCheckbox :v-model="areAllSelected" accent="info" @update:model-value="toggleSelect" />
</th>
<th v-else-if="column.id === 'more'" class="more">
<UiButtonIcon size="small" accent="info" :icon="faEllipsis" />
{{ column.label }}
</th>
<ColumnTitle v-else :id="column.id" :header-class="`col-${column.id}`" :icon="headerIcon[column.id]">
{{ column.label }}
</ColumnTitle>
</template>
</tr>
</template>
<template #tbody>
<tr
v-for="row of rows"
:key="row.id"
:class="{ selected: selectedNetworkId === row.id }"
@click="selectedNetworkId = row.id"
>
<td v-for="column of row.visibleColumns" :key="column.id" class="typo p2-regular">
<div v-if="column.id === 'checkbox'">
<UiCheckbox v-model="selected" accent="info" :value="row.id" />
</div>
<div v-else-if="column.id === 'status'" class="status">
<PifStatus :network="row.value" />
</div>
<div v-else-if="column.id === 'more'">
<VtsIcon accent="info" :icon="faEllipsis" />
</div>
<div v-else v-tooltip="{ placement: 'bottom-end' }" class="text-ellipsis">
{{ column.value }}
</div>
</td>
</tr>
</template>
</VtsDataTable>
<VtsStateHero v-if="searchQuery && filteredNetworks.length === 0" type="table" image="no-result">
<div>{{ $t('no-result') }}</div>
{{ $t('no-result') }}
</VtsStateHero>
<VtsStateHero v-if="networksByPool.length === 0" type="table" image="no-data">
<div>{{ $t('no-network-detected') }}</div>
</VtsStateHero>
<VtsStateHero v-if="hasError" type="table" image="error">
<div>{{ $t('error-no-data') }}</div>
</VtsStateHero>
<VtsLoadingHero v-if="!isReady" type="table" />
<div class="selection">
<UiTopBottomTable
:selected-items="selected.length"
Expand All @@ -103,14 +104,15 @@
<script setup lang="ts">
import PifStatus from '@/components/pif/PifStatus.vue'
import { useNetworkStore } from '@/stores/xo-rest-api/network.store'
import { usePifStore } from '@/stores/xo-rest-api/pif.store'
import type { XoNetwork } from '@/types/xo/network.type'
import type { XoPool } from '@/types/xo/pool.type'
import VtsErrorNoDataHero from '@core/components/state-hero/VtsErrorNoDataHero.vue'
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import ColumnTitle from '@core/components/table/ColumnTitle.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import VtsDataTable from '@core/components/table/VtsDataTable.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
import UiCheckbox from '@core/components/ui/checkbox/UiCheckbox.vue'
import UiDropdownButton from '@core/components/ui/dropdown-button/UiDropdownButton.vue'
import UiQuerySearchBar from '@core/components/ui/query-search-bar/UiQuerySearchBar.vue'
Expand All @@ -123,12 +125,12 @@ import useMultiSelect from '@core/composables/table/multi-select.composable'
import { useTable } from '@core/composables/table.composable'
import { vTooltip } from '@core/directives/tooltip.directive'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
import {
faAlignLeft,
faCaretDown,
faCopy,
faEdit,
faEllipsis,
faHashtag,
faPowerOff,
faTrash,
Expand All @@ -153,13 +155,16 @@ const { networksWithPifs, isReady, hasError } = useNetworkStore().subscribe()
const networksByPool = computed(() => networksWithPifs.value.filter(network => network.$pool === pool.id))
const { records: pifs } = usePifStore().subscribe()
const selectedNetworkId = useRouteQuery('id')
const findPageById = () => {
const index = networksByPool.value.findIndex(network => network.id === selectedNetworkId.value)
if (index === -1) {
return null
}
pagination.value.currentPage = Math.floor(index / pagination.value.pageSize) + 1
}
Expand All @@ -174,21 +179,34 @@ const searchQuery = ref('')
const filteredNetworks = computed(() => {
let filtered = networksByPool.value
if (!searchQuery.value) {
return networksByPool.value.slice(pagination.value.startIndex - 1, pagination.value.endIndex)
}
filtered = networksByPool.value.filter(network =>
Object.values(network).some(value => String(value).toLowerCase().includes(searchQuery.value.toLowerCase()))
)
return filtered.slice(pagination.value.startIndex - 1, pagination.value.endIndex)
})
const getNetworkVlan = (network: XoNetwork) => {
const networkPIFs = pifs.value.filter(pif => network.PIFs.includes(pif.id))
if (networkPIFs.length > 0) {
return networkPIFs[0].vlan !== -1 ? networkPIFs[0].vlan.toString() : t('none')
}
}
const getLockingMode = (lockingMode: boolean) => {
return lockingMode ? t('disabled') : t('unlocked')
}
const usableIds = computed(() => networksByPool.value.map(network => network.id))
const { selected, areAllSelected } = useMultiSelect(usableIds)
const toggleSelect = () => {
selected.value = selected.value.length === 0 ? usableIds.value : []
}
Expand All @@ -197,20 +215,22 @@ const { visibleColumns, rows } = useTable('networks', filteredNetworks, {
rowId: record => record.id,
columns: define => [
define('checkbox', () => '', { label: '', isHideable: false }),
define('name_label', (record: XoNetwork) => record.name_label, { label: t('name') }),
define('name_description', (record: { name_description: string }) => record.name_description, {
define('name_label', record => record.name_label, { label: t('name') }),
define('name_description', record => record.name_description, {
label: t('description'),
}),
define('status', () => '', { label: t('pifs-status') }),
define('vlan', () => '', { label: t('vlan') }),
define('vlan', record => getNetworkVlan(record), { label: 'VLAN' }),
define('MTU', record => record.MTU, { label: 'MTU' }),
define('default_locking_mode', (record: XoNetwork) => getLockingMode(record.defaultIsLocked), {
define('default_locking_mode', record => getLockingMode(record.defaultIsLocked), {
label: t('locking-mode-default'),
}),
define('more', () => '', { label: '', isHideable: false }),
],
})
type networkHeader = 'name_label' | 'name_description' | 'status' | 'vlan' | 'MTU' | 'default_locking_mode'
const headerIcon: Record<networkHeader, IconDefinition> = {
name_label: faAlignLeft,
name_description: faAlignLeft,
Expand All @@ -219,7 +239,6 @@ const headerIcon: Record<networkHeader, IconDefinition> = {
MTU: faHashtag,
default_locking_mode: faCaretDown,
}
const getHeaderIcon = (status: networkHeader) => headerIcon[status]
</script>

<style scoped lang="postcss">
Expand Down
1 change: 1 addition & 0 deletions @xen-orchestra/web/src/pages/pool/[id]/networks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ defineProps<{
<style scoped lang="postcss">
.networks {
display: flex;
.card {
margin: 0.8rem;
}
Expand Down
1 change: 1 addition & 0 deletions @xen-orchestra/web/src/stores/xo-rest-api/network.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ export const useNetworkStore = defineStore('network', () => {
networksWithPifs,
networksWithoutPifs,
}

return createSubscribableStoreContext({ context, ...configRest }, {})
})

0 comments on commit 78092ea

Please sign in to comment.