feat(xo6/network): add dedicated network page (#10704)

This commit is contained in:
Joris K
2026-11-09 09:39:47 +01:00
committed by GitHub
parent e4cc8df323
commit 8cc3384850
18 changed files with 513 additions and 83 deletions

View File

@@ -284,6 +284,7 @@
"bond-mode": "Bond mode",
"bond-mode-required": "Bond mode is required.",
"bond-status": "Bond status",
"bonded-network": "Bonded network",
"boot-firmware": "Boot firmware",
"bootable": "Bootable",
"br-details": "Backup repository details",
@@ -538,6 +539,7 @@
"interface": "Interface",
"interface-required": "Interface is required.",
"interfaces": "Interfaces",
"internal-network": "Internal network",
"interrupted": "Interrupted",
"invalid-field": "Invalid field",
"invalid-ip-addresses": "One or more invalid IP addresses",
@@ -1077,6 +1079,7 @@
"pbd-details": "Physical Block Device (PBD) details",
"pending": "Pending",
"physical-interface-status": "Physical interface status",
"physical-network": "Physical network",
"pif": "PIF",
"pif-status": "PIF status",
"pifs": "PIFs",

View File

@@ -284,6 +284,7 @@
"bond-mode": "Mode de liaison",
"bond-mode-required": "Le mode de liaison est requis.",
"bond-status": "Statut du bond",
"bonded-network": "Réseau lié",
"boot-firmware": "Firmware de démarrage",
"bootable": "Amorçable",
"br-details": "Détails du BR",
@@ -538,6 +539,7 @@
"interface": "Interface",
"interface-required": "L'interface est requise.",
"interfaces": "Interfaces",
"internal-network": "Réseau interne",
"interrupted": "Interrompu",
"invalid-field": "Champ invalide",
"invalid-ip-addresses": "Une ou plusieurs adresses IPs invalides",
@@ -1077,6 +1079,7 @@
"pbd-details": "Détails du périphérique de bloc physique (PBD)",
"pending": "En cours",
"physical-interface-status": "Statut de l'interface physique",
"physical-network": "Réseau physique",
"pif": "PIF",
"pif-status": "Statut du PIF",
"pifs": "PIFs",

View File

@@ -0,0 +1,79 @@
<template>
<div class="breadcrumb-container">
<UiBreadcrumb :size>
<UiLink v-if="pool" :size :to="{ name: '/pool/[id]/dashboard', params: { id: pool.id } }" icon="object:pool">
{{ pool.name_label }}
</UiLink>
<UiLink :size :to="{ name: '/pool/[id]/networks', params: { id: network.$pool } }">
{{ t('network') }}
</UiLink>
<span>
<VtsIcon name="object:network" size="current" />
{{ network.name_label }}
</span>
</UiBreadcrumb>
</div>
<UiHeadBar icon="object:network">
{{ network.name_label }}
</UiHeadBar>
<TabList>
<RouterLink v-slot="{ isActive, href }" :to="{ name: '/network/[id]/general', params: { id: network.id } }" custom>
<UiTabItem :active="isActive" :href tag="a">
{{ t('general') }}
</UiTabItem>
</RouterLink>
<RouterLink
v-if="!isInternalNetwork"
v-slot="{ isActive, href }"
:to="{ name: '/network/[id]/pifs', params: { id: network.id } }"
custom
>
<UiTabItem :active="isActive" :href tag="a">
{{ t('pifs') }}
</UiTabItem>
</RouterLink>
</TabList>
</template>
<script setup lang="ts">
import { useXoNetworkUtils } from '@/modules/network/composables/xo-network-utils.composable.ts'
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
import { useXoPoolCollection } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import TabList from '@core/components/tab-list/TabList.vue'
import UiBreadcrumb from '@core/components/ui/breadcrumb/UiBreadcrumb.vue'
import UiHeadBar from '@core/components/ui/head-bar/UiHeadBar.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiTabItem from '@core/components/ui/tab-item/UiTabItem.vue'
import { useUiStore } from '@core/stores/ui.store.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { network } = defineProps<{ network: FrontXoNetwork }>()
const { t } = useI18n()
const uiStore = useUiStore()
const size = computed(() => (uiStore.isSmall ? 'small' : 'medium'))
const { useGetPoolById } = useXoPoolCollection()
const pool = useGetPoolById(() => network.$pool)
const { isInternalNetwork } = useXoNetworkUtils(() => network)
</script>
<style lang="postcss" scoped>
.breadcrumb-container {
min-height: 5.6rem;
padding: 1.2rem 1.6rem;
display: flex;
gap: 1.6rem;
align-items: center;
border-bottom: 0.1rem solid var(--color-neutral-border);
background-color: var(--color-neutral-background-primary);
justify-content: space-between;
overflow-y: auto;
}
</style>

View File

@@ -26,10 +26,10 @@
<script setup lang="ts">
import { useNetworkDelete } from '@/modules/network/composables/use-network-delete.composable.ts'
import { useXoNetworkUtils } from '@/modules/network/composables/xo-network-utils.composable.ts'
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
import { getNetworkStatus } from '@/modules/network/utils/xo-network.util.ts'
import { getNetworkRoute, getNetworkStatus } from '@/modules/network/utils/xo-network.util.ts'
import { useXoPifCollection } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
import VtsQueryBuilder from '@core/components/query-builder/VtsQueryBuilder.vue'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
@@ -65,7 +65,7 @@ defineSlots<{
const { t } = useI18n()
const { pifs, getPifsByIds } = useXoPifCollection()
const { getPifsByIds } = useXoPifCollection()
const selectedNetworkId = useRouteQuery('id')
@@ -93,29 +93,15 @@ const state = useTableState({
: false,
})
const getNetworkVlan = (network: FrontXoNetwork) => {
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 = (isLocked: boolean) => (isLocked ? t('disabled') : t('unlocked'))
const { pageRecords: paginatedNetworks, paginationBindings } = usePagination('networks', filteredNetworks)
const { HeadCells, BodyCells } = useNetworkColumns({
exclude: internal ? ['vlan', 'status', 'selectItem'] : ['selectItem'],
body: (network: FrontXoNetwork) => {
const { buildXo5Route } = useXoRoutes()
const networkPifs = computed(() => getPifsByIds(network.PIFs))
const status = computed(() => getNetworkStatus(networkPifs.value))
const vlan = computed(() => getNetworkVlan(network))
const defaultLockingMode = computed(() => getLockingMode(network.defaultIsLocked))
const href = computed(() => buildXo5Route(`/pools/${network.$pool}/network?s=1_0_asc-${network.id}`))
const { networkVlan, networkDefaultLockingMode } = useXoNetworkUtils(network)
const { deleteNetworks, isDeletingNetworks } = useNetworkDelete(() => [network])
@@ -124,13 +110,13 @@ const { HeadCells, BodyCells } = useNetworkColumns({
r({
label: network.name_label,
icon: icon('object:network'),
href: href.value,
to: getNetworkRoute(network.id),
}),
description: r => r(network.name_description),
status: r => r(status.value),
vlan: r => r(vlan.value),
vlan: r => r(networkVlan.value),
mtu: r => r(network.MTU),
defaultLockingMode: r => r(defaultLockingMode.value),
defaultLockingMode: r => r(networkDefaultLockingMode.value),
actions: r =>
r({
onClick: () => (selectedNetworkId.value = network.id),

View File

@@ -0,0 +1,38 @@
<template>
<UiCard>
<UiTitle>
{{ t('general-information') }}
</UiTitle>
<VtsTabularKeyValueList>
<VtsTabularKeyValueRow :label="t('name')" :value="network.name_label" />
<VtsTabularKeyValueRow :label="t('uuid')">
<template #value>
<VtsCodeSnippet :content="network.id" copy />
</template>
</VtsTabularKeyValueRow>
<VtsTabularKeyValueRow :label="t('description')" :value="network.name_description" />
<VtsTabularKeyValueRow :label="t('type')" :value="networkTypeLabel" />
<VtsTabularKeyValueRow :label="t('vlan')" :value="networkVlan" />
<VtsTabularKeyValueRow :label="t('mtu')" :value="String(network.MTU)" />
<VtsTabularKeyValueRow :label="t('network-block-device')" :value="networkNbd" />
<VtsTabularKeyValueRow :label="t('locking-mode-default')" :value="networkDefaultLockingMode" />
</VtsTabularKeyValueList>
</UiCard>
</template>
<script setup lang="ts">
import { useXoNetworkUtils } from '@/modules/network/composables/xo-network-utils.composable.ts'
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
import VtsCodeSnippet from '@core/components/code-snippet/VtsCodeSnippet.vue'
import VtsTabularKeyValueList from '@core/components/tabular-key-value-list/VtsTabularKeyValueList.vue'
import VtsTabularKeyValueRow from '@core/components/tabular-key-value-row/VtsTabularKeyValueRow.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { useI18n } from 'vue-i18n'
const { network } = defineProps<{ network: FrontXoNetwork }>()
const { t } = useI18n()
const { networkVlan, networkNbd, networkDefaultLockingMode, networkTypeLabel } = useXoNetworkUtils(() => network)
</script>

View File

@@ -10,7 +10,7 @@
size="medium"
:label="network.name_label"
icon="object:network"
:href="networkHref"
:to="networkTo"
/>
<VtsCardRowList class="content">
<!-- DESCRIPTION -->
@@ -66,9 +66,9 @@
<script setup lang="ts">
import NetworkPifsInfoCard from '@/modules/network/components/panel/cards/NetworkPifsInfoCard.vue'
import { useNetworkDelete } from '@/modules/network/composables/use-network-delete.composable.ts'
import { useXoNetworkUtils } from '@/modules/network/composables/xo-network-utils.composable.ts'
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
import { useXoPifCollection } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
import { getNetworkRoute } from '@/modules/network/utils/xo-network.util.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
@@ -89,29 +89,11 @@ const emit = defineEmits<{
const { deleteNetworks, isDeletingNetworks } = useNetworkDelete(() => (network !== undefined ? [network] : []))
const { buildXo5Route } = useXoRoutes()
const networkHref = computed(() =>
network !== undefined ? buildXo5Route(`/pools/${network.$pool}/network?s=1_0_asc-${network.id}`) : undefined
)
const { getPifsByNetworkId } = useXoPifCollection()
const networkTo = computed(() => (network !== undefined ? getNetworkRoute(network.id) : undefined))
const { t } = useI18n()
const pifs = computed(() => (network !== undefined ? getPifsByNetworkId(network.id) : []))
const networkVlan = computed(() => {
if (pifs.value.length === 0) {
return
}
return pifs.value[0].vlan !== -1 ? pifs.value[0].vlan.toString() : t('none')
})
const networkNbd = computed(() => (network?.nbd ? t('on') : t('off')))
const networkDefaultLockingMode = computed(() => (network?.defaultIsLocked ? t('disabled') : t('unlocked')))
const { networkVlan, networkNbd, networkDefaultLockingMode } = useXoNetworkUtils(() => network)
</script>
<style scoped lang="postcss">

View File

@@ -0,0 +1,51 @@
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
import { getNetworkType, NETWORK_TYPE } from '@/modules/network/utils/xo-network.util.ts'
import { useXoPifCollection } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import { useMapper } from '@core/packages/mapper'
import { toComputed } from '@core/utils/to-computed.util.ts'
import { computed, type MaybeRefOrGetter } from 'vue'
import { useI18n } from 'vue-i18n'
export function useXoNetworkUtils(rawNetwork: MaybeRefOrGetter<FrontXoNetwork | undefined>) {
const { t } = useI18n()
const network = toComputed(rawNetwork)
const { getPifsByNetworkId } = useXoPifCollection()
const pifs = computed(() => (network.value !== undefined ? getPifsByNetworkId(network.value.id) : []))
const networkVlan = computed(() => {
if (pifs.value.length === 0) {
return
}
return pifs.value[0].vlan !== -1 ? pifs.value[0].vlan.toString() : t('none')
})
const networkNbd = computed(() => (network.value?.nbd ? t('on') : t('off')))
const networkDefaultLockingMode = computed(() => (network.value?.defaultIsLocked ? t('disabled') : t('unlocked')))
const networkType = computed(() => (network.value !== undefined ? getNetworkType(network.value) : undefined))
const networkTypeLabel = useMapper(
networkType,
{
[NETWORK_TYPE.BONDED]: t('bonded-network'),
[NETWORK_TYPE.INTERNAL]: t('internal-network'),
[NETWORK_TYPE.PHYSICAL]: t('physical-network'),
},
NETWORK_TYPE.PHYSICAL
)
const isInternalNetwork = computed(() => networkType.value === NETWORK_TYPE.INTERNAL)
return {
networkVlan,
networkNbd,
networkDefaultLockingMode,
networkTypeLabel,
isInternalNetwork,
}
}

View File

@@ -0,0 +1,133 @@
import { useXoNetworkUtils } from '@/modules/network/composables/xo-network-utils.composable.ts'
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
import type { FrontXoPif, useXoPifCollection } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import { createNetwork } from '@/test/create-network.ts'
import { createPif } from '@/test/create-pif.ts'
import { mountComposable } from '@/test/mount-composable.ts'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { pifs } = vi.hoisted(() => ({
pifs: { value: [] as FrontXoPif[] },
}))
vi.mock(import('@/modules/pif/remote-resources/use-xo-pif-collection.ts'), () => ({
useXoPifCollection: (() => ({
getPifsByNetworkId: (networkId: FrontXoNetwork['id']) => pifs.value.filter(pif => pif.$network === networkId),
})) as unknown as typeof useXoPifCollection,
}))
beforeEach(() => {
pifs.value = []
})
function mountNetworkUtils(network: FrontXoNetwork | undefined) {
return mountComposable(() => {
const { t } = useI18n()
return { ...useXoNetworkUtils(network), t }
}).wrapper.vm
}
describe('networkVlan', () => {
it('is the vlan of the first pif of the network', () => {
pifs.value = [createPif({ vlan: 100 }), createPif({ vlan: 200 })]
expect(mountNetworkUtils(createNetwork()).networkVlan).toBe('100')
})
it('is none when the first pif has no vlan', () => {
pifs.value = [createPif({ vlan: -1 })]
const { networkVlan, t } = mountNetworkUtils(createNetwork())
expect(networkVlan).toBe(t('none'))
})
it('ignores the pifs of other networks', () => {
pifs.value = [createPif({ $network: 'network-other' as FrontXoPif['$network'], vlan: 100 })]
expect(mountNetworkUtils(createNetwork()).networkVlan).toBeUndefined()
})
it('is undefined when there is no network', () => {
pifs.value = [createPif({ vlan: 100 })]
expect(mountNetworkUtils(undefined).networkVlan).toBeUndefined()
})
})
describe('networkNbd', () => {
it('is on when nbd is enabled', () => {
const { networkNbd, t } = mountNetworkUtils(createNetwork({ nbd: true }))
expect(networkNbd).toBe(t('on'))
})
it('is off when nbd is disabled', () => {
const { networkNbd, t } = mountNetworkUtils(createNetwork({ nbd: false }))
expect(networkNbd).toBe(t('off'))
})
})
describe('networkDefaultLockingMode', () => {
it('is disabled when the network is locked by default', () => {
const { networkDefaultLockingMode, t } = mountNetworkUtils(createNetwork({ defaultIsLocked: true }))
expect(networkDefaultLockingMode).toBe(t('disabled'))
})
it('is unlocked when the network is not locked by default', () => {
const { networkDefaultLockingMode, t } = mountNetworkUtils(createNetwork({ defaultIsLocked: false }))
expect(networkDefaultLockingMode).toBe(t('unlocked'))
})
})
describe('networkTypeLabel', () => {
it('is bonded for a bonded network', () => {
const { networkTypeLabel, t } = mountNetworkUtils(
createNetwork({ isBonded: true, PIFs: ['pif-123' as FrontXoPif['id']] })
)
expect(networkTypeLabel).toBe(t('bonded-network'))
})
it('is internal for a network without pif', () => {
const { networkTypeLabel, t } = mountNetworkUtils(createNetwork({ PIFs: [] }))
expect(networkTypeLabel).toBe(t('internal-network'))
})
it('is physical for a network with pifs', () => {
const { networkTypeLabel, t } = mountNetworkUtils(createNetwork({ PIFs: ['pif-123' as FrontXoPif['id']] }))
expect(networkTypeLabel).toBe(t('physical-network'))
})
})
describe('isInternalNetwork', () => {
it('is true for a network without pif', () => {
expect(mountNetworkUtils(createNetwork({ PIFs: [] })).isInternalNetwork).toBe(true)
})
it('is false for a network with pifs', () => {
expect(mountNetworkUtils(createNetwork({ PIFs: ['pif-123' as FrontXoPif['id']] })).isInternalNetwork).toBe(false)
})
it('is false when there is no network', () => {
expect(mountNetworkUtils(undefined).isInternalNetwork).toBe(false)
})
it('follows the network', () => {
const network = ref(createNetwork({ PIFs: [] }))
const { wrapper } = mountComposable(() => useXoNetworkUtils(network))
expect(wrapper.vm.isInternalNetwork).toBe(true)
network.value = createNetwork({ PIFs: ['pif-123' as FrontXoPif['id']] })
expect(wrapper.vm.isInternalNetwork).toBe(false)
})
})

View File

@@ -24,6 +24,13 @@ export function getPoolNetworkRoute(
}
}
export function getNetworkRoute(networkId: FrontXoNetwork['id']): RouteLocationAsRelative {
return {
name: '/network/[id]',
params: { id: networkId },
}
}
export function getNetworkStatus(pifs: FrontXoPif[]) {
return getConnectionStatus(pifs.map(pif => pif.attached && pif.carrier))
}

View File

@@ -0,0 +1,11 @@
import { getNetworkRoute } from '@/modules/network/utils/xo-network.util.ts'
import { createNetwork } from '@/test/create-network.ts'
describe('getNetworkRoute', () => {
it('is the page of the network', () => {
expect(getNetworkRoute(createNetwork().id)).toEqual({
name: '/network/[id]',
params: { id: 'network-123' },
})
})
})

View File

@@ -6,7 +6,7 @@
<slot name="title-actions" />
</template>
</UiTitle>
<UiQuerySearchBar @search="(value: string) => (searchQuery = value)" />
<VtsQueryBuilder :model-value="filter" :schema @search="filter = $event" />
<div class="container">
<VtsTable :state :pagination-bindings sticky="right">
<thead>
@@ -30,19 +30,24 @@ import { getPoolNetworkRoute } from '@/modules/network/utils/xo-network.util.ts'
import { usePifManagementReconfigureModal } from '@/modules/pif/composables/use-pif-management-reconfigure-modal.composable.ts'
import { type FrontXoPif, useXoPifCollection } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import { getPifStatus } from '@/modules/pif/utils/xo-pif.util.ts'
import VtsQueryBuilder from '@core/components/query-builder/VtsQueryBuilder.vue'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
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 } from '@core/icons'
import { useQueryBuilderSchema } from '@core/packages/query-builder/schema/use-query-builder-schema.ts'
import { useQueryBuilderFilter } from '@core/packages/query-builder/use-query-builder-filter.ts'
import { usePifColumns } from '@core/tables/column-sets/pif-columns.ts'
import { renderBodyCell } from '@core/tables/helpers/render-body-cell.ts'
import type { IP_CONFIGURATION_MODE } from '@vates/types'
import { useBooleanSchema } from '@core/utils/query-builder/use-boolean-schema.ts'
import { useNumberSchema } from '@core/utils/query-builder/use-number-schema.ts'
import { useStringSchema } from '@core/utils/query-builder/use-string-schema.ts'
import { IP_CONFIGURATION_MODE } from '@vates/types'
import { logicAnd, logicNot, logicOr } from '@vueuse/math'
import { computed, ref } from 'vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { pifs: rawPifs } = defineProps<{
@@ -63,16 +68,24 @@ const hasError = logicOr(hasPifFetchError, hasNetworkFetchError)
const { t } = useI18n()
const selectedPifId = useRouteQuery('id')
const searchQuery = ref('')
const filteredPifs = computed(() => {
const searchTerm = searchQuery.value.trim().toLocaleLowerCase()
const { items: filteredPifs, filter } = useQueryBuilderFilter('pifs', () => rawPifs)
if (!searchTerm) {
return rawPifs
}
return rawPifs.filter(pif => Object.values(pif).some(value => String(value).toLocaleLowerCase().includes(searchTerm)))
const schema = useQueryBuilderSchema<FrontXoPif>({
'': useStringSchema(t('any-property')),
device: useStringSchema(t('device')),
attached: useBooleanSchema(t('status'), {
true: t('connected'),
false: t('disconnected'),
}),
vlan: useNumberSchema(t('vlan')),
ip: useStringSchema(t('ip-address')),
mac: useStringSchema(t('mac-address')),
mode: useStringSchema(t('ip-mode'), {
[IP_CONFIGURATION_MODE.STATIC]: t('static'),
[IP_CONFIGURATION_MODE.DHCP]: t('dhcp'),
[IP_CONFIGURATION_MODE.NONE]: t('none'),
}),
})
const state = useTableState({

View File

@@ -87,9 +87,9 @@
</template>
<script setup lang="ts">
import { useXoNetworkUtils } from '@/modules/network/composables/xo-network-utils.composable.ts'
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
import { getPoolNetworkRoute } from '@/modules/network/utils/xo-network.util.ts'
import { useXoPifCollection } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
@@ -109,19 +109,5 @@ const { t } = useI18n()
const networkTo = computed(() => getPoolNetworkRoute(network.$pool, network.id))
const { getPifsByNetworkId } = useXoPifCollection()
const pifs = computed(() => getPifsByNetworkId(network.id))
const networkVlan = computed(() => {
if (pifs.value.length === 0) {
return
}
return pifs.value[0].vlan !== -1 ? pifs.value[0].vlan.toString() : t('none')
})
const networkNbd = computed(() => (network.nbd ? t('on') : t('off')))
const networkDefaultLockingMode = computed(() => (network.defaultIsLocked ? t('disabled') : t('unlocked')))
const { networkVlan, networkNbd, networkDefaultLockingMode } = useXoNetworkUtils(() => network)
</script>

View File

@@ -41,9 +41,9 @@
</template>
<script setup lang="ts">
import { useXoNetworkUtils } from '@/modules/network/composables/xo-network-utils.composable.ts'
import { useXoNetworkCollection } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
import { getPoolNetworkRoute } from '@/modules/network/utils/xo-network.util.ts'
import { useXoPifCollection } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import { useXoVifUtils } from '@/modules/vif/composables/xo-vif-utils.composable.ts'
import type { FrontXoVif } from '@/modules/vif/remote-resources/use-xo-vif-collection.ts'
import VtsCodeSnippet from '@core/components/code-snippet/VtsCodeSnippet.vue'
@@ -63,7 +63,6 @@ const { vif } = defineProps<{ vif: FrontXoVif }>()
const { t } = useI18n()
const { useGetNetworkById } = useXoNetworkCollection()
const { getPifsByNetworkId } = useXoPifCollection()
const { lockingModeLabel } = useXoVifUtils(() => vif)
const network = useGetNetworkById(() => vif.$network)
@@ -72,15 +71,7 @@ const networkTo = computed(() =>
network.value ? getPoolNetworkRoute(network.value.$pool, network.value.id) : undefined
)
const pifs = computed(() => getPifsByNetworkId(vif.$network))
const networkVlan = computed(() => {
if (pifs.value.length === 0) {
return
}
return pifs.value[0].vlan !== -1 ? pifs.value[0].vlan.toString() : t('none')
})
const { networkVlan } = useXoNetworkUtils(network)
const vifStatus = computed(() => (vif.attached ? CONNECTION_STATUS.CONNECTED : CONNECTION_STATUS.DISCONNECTED))

View File

@@ -0,0 +1,35 @@
<template>
<VtsStateHero v-if="!areNetworksReady" format="page" type="busy" size="large" />
<VtsStateHero v-else-if="!network" format="page" type="not-found" size="large">
{{ t('object-not-found', { id: route.params.id }) }}
</VtsStateHero>
<RouterView v-else v-slot="{ Component }">
<NetworkHeader v-if="uiStore.hasUi" :network />
<component :is="Component" :network />
</RouterView>
</template>
<script lang="ts" setup>
import NetworkHeader from '@/modules/network/components/NetworkHeader.vue'
import {
type FrontXoNetwork,
useXoNetworkCollection,
} from '@/modules/network/remote-resources/use-xo-network-collection.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import { useDefaultTab } from '@core/composables/default-tab.composable.ts'
import { useUiStore } from '@core/stores/ui.store.ts'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
useDefaultTab('/network/[id]', 'general')
const route = useRoute<'/network/[id]'>()
const { t } = useI18n()
const uiStore = useUiStore()
const { areNetworksReady, useGetNetworkById } = useXoNetworkCollection()
const network = useGetNetworkById(() => route.params.id as FrontXoNetwork['id'])
</script>

View File

@@ -0,0 +1,16 @@
<template>
<VtsColumns :columns="1" extra-space-around>
<VtsColumn>
<NetworkGeneralInfoCard :network />
</VtsColumn>
</VtsColumns>
</template>
<script setup lang="ts">
import NetworkGeneralInfoCard from '@/modules/network/components/cards/NetworkGeneralInfoCard.vue'
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
import VtsColumn from '@core/components/column/VtsColumn.vue'
import VtsColumns from '@core/components/columns/VtsColumns.vue'
defineProps<{ network: FrontXoNetwork }>()
</script>

View File

@@ -0,0 +1,53 @@
<template>
<VtsContentSidePanel class="pifs">
<UiCard class="container">
<PifsTable :pifs />
</UiCard>
<PifSidePanel :pif="selectedPif" @close="selectedPif = undefined" />
</VtsContentSidePanel>
</template>
<script setup lang="ts">
import { useXoNetworkUtils } from '@/modules/network/composables/xo-network-utils.composable.ts'
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
import PifSidePanel from '@/modules/pif/components/list/panel/PifSidePanel.vue'
import PifsTable from '@/modules/pif/components/list/PifsTable.vue'
import { useXoPifCollection, type FrontXoPif } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import VtsContentSidePanel from '@core/components/layout/VtsContentSidePanel.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
import { whenever } from '@vueuse/core'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
const { network } = defineProps<{
network: FrontXoNetwork
}>()
const router = useRouter()
const { isInternalNetwork } = useXoNetworkUtils(() => network)
whenever(isInternalNetwork, () => router.replace({ name: '/network/[id]/general', params: { id: network.id } }), {
immediate: true,
})
const { getPifsByNetworkId } = useXoPifCollection()
const pifs = computed(() => getPifsByNetworkId(network.id))
const selectedPif = useRouteQuery<FrontXoPif | undefined>('id', {
toData: id => pifs.value.find(pif => pif.id === id),
toQuery: pif => pif?.id ?? '',
})
</script>
<style scoped lang="postcss">
.pifs {
.container {
height: fit-content;
margin: 0.8rem;
gap: 4rem;
}
}
</style>

View File

@@ -252,6 +252,28 @@ declare module 'vue-router/auto-routes' {
{ id: ParamValue<false> },
| never
>,
'/network/[id]': RouteRecordInfo<
'/network/[id]',
'/network/:id',
{ id: ParamValue<true> },
{ id: ParamValue<false> },
| '/network/[id]/general'
| '/network/[id]/pifs'
>,
'/network/[id]/general': RouteRecordInfo<
'/network/[id]/general',
'/network/:id/general',
{ id: ParamValue<true> },
{ id: ParamValue<false> },
| never
>,
'/network/[id]/pifs': RouteRecordInfo<
'/network/[id]/pifs',
'/network/:id/pifs',
{ id: ParamValue<true> },
{ id: ParamValue<false> },
| never
>,
'/network/new': RouteRecordInfo<
'/network/new',
'/network/new',
@@ -737,6 +759,26 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
'src/pages/network/[id].vue': {
routes:
| '/network/[id]'
| '/network/[id]/general'
| '/network/[id]/pifs'
views:
| 'default'
}
'src/pages/network/[id]/general.vue': {
routes:
| '/network/[id]/general'
views:
| never
}
'src/pages/network/[id]/pifs.vue': {
routes:
| '/network/[id]/pifs'
views:
| never
}
'src/pages/network/new.vue': {
routes:
| '/network/new'

View File

@@ -14,6 +14,7 @@
- [Side panels] Side panels can be resized like the treeview, whose default width now adapts to the screen size (PR [#10725](https://github.com/vatesfr/xen-orchestra/pull/10725))
- [Side panels] Values stay next to their label when they fit and move below it otherwise, fields with several values (IP addresses, report recipients…) get a copy button per value and a "Copy all" action, and rows, links and statuses are more evenly spaced and sized (PR [#10733](https://github.com/vatesfr/xen-orchestra/pull/10733))
- [Treeview] A more compact tree, so that names get more room: lower rows running edge to edge and a smaller indent per level; hovering a quick actions button no longer highlights its whole row (PR [#10727](https://github.com/vatesfr/xen-orchestra/pull/10727))
- [Pool/Network] Add network dedicated page, with General and PIFs tabs (PR [#10704](https://github.com/vatesfr/xen-orchestra/pull/10704))
### Bug fixes