mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
feat(xo6/network): add dedicated network page (#10704)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
35
@xen-orchestra/web/src/pages/network/[id].vue
Normal file
35
@xen-orchestra/web/src/pages/network/[id].vue
Normal 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>
|
||||
16
@xen-orchestra/web/src/pages/network/[id]/general.vue
Normal file
16
@xen-orchestra/web/src/pages/network/[id]/general.vue
Normal 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>
|
||||
53
@xen-orchestra/web/src/pages/network/[id]/pifs.vue
Normal file
53
@xen-orchestra/web/src/pages/network/[id]/pifs.vue
Normal 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>
|
||||
42
@xen-orchestra/web/src/route-map.d.ts
vendored
42
@xen-orchestra/web/src/route-map.d.ts
vendored
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user