Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(xo-lite/host): add host network tab and pif table / status component … #8180

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion @xen-orchestra/lite/src/components/host/HostTabBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<RouterTab :to="{ name: 'host.console', params: { uuid } }">
{{ $t('console') }}
</RouterTab>
<RouterTab :to="{ name: 'host.network', params: { uuid } }" disabled>
<RouterTab :to="{ name: 'host.network', params: { uuid } }">
{{ $t('network') }}
</RouterTab>
<RouterTab :to="{ name: 'host.tasks', params: { uuid } }" disabled>
Expand Down
250 changes: 250 additions & 0 deletions @xen-orchestra/lite/src/components/host/network/HostPifTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
<template>
<div class="host-pif-table">
<UiTitle>
{{ $t('pifs') }}
<template #actions>
<UiButton
v-tooltip="$t('coming-soon')"
disabled
:left-icon="faPlus"
variant="secondary"
accent="info"
size="medium"
>
{{ $t('scan-pifs') }}
</UiButton>
</template>
</UiTitle>
<div class="container">
<UiQuerySearchBar class="table-query" @search="(value: string) => (searchQuery = value)" />
<UiTableActions title="Table actions">
<UiButton
v-tooltip="$t('coming-soon')"
disabled
:left-icon="faEdit"
variant="tertiary"
accent="info"
size="medium"
>
{{ $t('edit') }}
</UiButton>
<UiButton
v-tooltip="$t('coming-soon')"
disabled
:left-icon="faTrash"
variant="tertiary"
accent="danger"
size="medium"
>
{{ $t('delete') }}
</UiButton>
</UiTableActions>
<UiTopBottomTable
:selected-items="selected.length"
:total-items="pifsUuids.length"
@toggle-select-all="toggleSelect"
/>
<div class="table">
<VtsTable 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>
<th v-else-if="column.id === 'more'" class="more">
<UiButtonIcon size="small" accent="info" :icon="getHeaderIcon(column.id)" />
{{ column.label }}
</th>
<ColumnTitle v-else id="networks" :icon="getHeaderIcon(column.id)"> {{ column.label }}</ColumnTitle>
</template>
</tr>
</thead>
<tbody>
<tr v-for="row of rows" :key="row.id">
<td v-for="column of row.visibleColumns" :key="column.id" class="typo p2-regular">
<UiCheckbox v-if="column.id === 'checkbox'" v-model="selected" accent="info" :value="row.id" />
<VtsIcon v-else-if="column.id === 'more'" accent="info" :icon="faEllipsis" />
<div v-else-if="column.id === 'status'" v-tooltip>
<VtsConnectionStatus :status="column.value" />
</div>
<div v-else-if="column.id === 'network'" class="network">
<UiComplexIcon size="medium" class="icon">
<VtsIcon :icon="faNetworkWired" accent="current" />
<VtsIcon accent="success" :icon="faCircle" :overlay-icon="faCheck" />
</UiComplexIcon>
<a v-tooltip href="" class="text-ellipsis name">{{ column.value }}</a>
</div>
<div v-else v-tooltip="{ placement: 'bottom-end' }" class="text-ellipsis">
{{ column.value }}
</div>
</td>
</tr>
</tbody>
</VtsTable>
</div>
<UiTopBottomTable
:selected-items="selected.length"
:total-items="pifsUuids.length"
@toggle-select-all="toggleSelect"
/>
</div>
</div>
<UiCardSpinner v-if="!isReady" />
</template>

<script lang="ts" setup>
import UiCardSpinner from '@/components/ui/UiCardSpinner.vue'
import useMultiSelect from '@/composables/multi-select.composable'
import type { XenApiNetwork, XenApiPif } from '@/libs/xen-api/xen-api.types'
import { useNetworkStore } from '@/stores/xen-api/network.store'
import { usePifMetricsStore } from '@/stores/xen-api/pif-metrics.store'
import VtsConnectionStatus from '@core/components/connection-status/VtsConnectionStatus.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import ColumnTitle from '@core/components/table/ColumnTitle.vue'
import VtsTable from '@core/components/table/VtsTable.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 UiComplexIcon from '@core/components/ui/complex-icon/UiComplexIcon.vue'
import UiQuerySearchBar from '@core/components/ui/query-search-bar/UiQuerySearchBar.vue'
import UiTableActions from '@core/components/ui/table-actions/UiTableActions.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import UiTopBottomTable from '@core/components/ui/top-bottom-table/UiTopBottomTable.vue'
import { useTable } from '@core/composables/table.composable'
import { vTooltip } from '@core/directives/tooltip.directive'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
import {
faAlignLeft,
faAt,
faCaretDown,
faCheck,
faCircle,
faEdit,
faEllipsis,
faNetworkWired,
faPlus,
faPowerOff,
faTrash,
} from '@fortawesome/free-solid-svg-icons'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'

const { pifs } = defineProps<{
pifs: XenApiPif[]
isReady: boolean
}>()

const { t } = useI18n()

const { getByOpaqueRef } = useNetworkStore().subscribe()
const { getPifCarrier } = usePifMetricsStore().subscribe()

const getNetworkName = (networkRef: string) => {
const network: XenApiNetwork = getByOpaqueRef(networkRef as XenApiNetwork['$ref'])!
return network?.name_label ? network.name_label : ''
}

const getVlanData = (vlan: number) => {
return vlan !== -1 ? vlan : t('none')
}

const getPifStatus = (pif: XenApiPif) => {
const carrier = getPifCarrier(pif)
const currentlyAttached = pif.currently_attached

if (currentlyAttached && carrier) {
return 'connected'
}
if (currentlyAttached && !carrier) {
return 'partially-connected'
}
return 'disconnected'
}

const searchQuery = ref('')

const filteredNetworks = computed(() => {
const searchTerm = searchQuery.value.trim().toLocaleLowerCase()
if (!searchTerm) {
return pifs
}
return pifs.filter(pif => Object.values(pif).some(value => String(value).toLocaleLowerCase().includes(searchTerm)))
})

const pifsUuids = computed(() => pifs.map(pif => pif.uuid))

const { selected, areAllSelected } = useMultiSelect(pifsUuids)

const toggleSelect = () => {
selected.value = selected.value.length === 0 ? pifsUuids.value : []
}

const { visibleColumns, rows } = useTable('pifs', filteredNetworks, {
rowId: record => record.uuid,
columns: define => [
define('checkbox', () => '', { label: '', isHideable: false }),
define('network', record => getNetworkName(record.network), { label: 'Network', isHideable: true }),
define('device', { label: 'Device', isHideable: true }),
define('status', record => getPifStatus(record), { label: 'Status', isHideable: true }),
define('VLAN', record => getVlanData(record.VLAN), { label: 'Vlan', isHideable: true }),
define('IP', { label: 'IP', isHideable: true }),
define('MAC', { label: 'MAC', isHideable: true }),
define('ip_configuration_mode', { label: 'Mode', isHideable: true }),
define('more', () => '', { label: '', isHideable: false }),
],
})
type PifHeader = 'network' | 'device' | 'status' | 'VLAN' | 'IP' | 'MAC' | 'ip_configuration_mode' | 'more'
const headerIcon: Record<PifHeader, { icon: IconDefinition }> = {
network: { icon: faAlignLeft },
device: { icon: faAlignLeft },
status: { icon: faPowerOff },
VLAN: { icon: faAlignLeft },
IP: { icon: faAt },
MAC: { icon: faAt },
ip_configuration_mode: { icon: faCaretDown },
more: { icon: faEllipsis },
}
const getHeaderIcon = (status: PifHeader) => headerIcon[status].icon
</script>

<style scoped lang="postcss">
.host-pif-table,
.container {
display: flex;
flex-direction: column;
}

.host-pif-table {
gap: 2.4rem;

.container {
gap: 0.8rem;

.table {
overflow-x: auto;

.network {
display: flex;
align-items: center;
gap: 1.8rem;
}

tr:last-child {
border-bottom: 1px solid var(--color-neutral-border);
}
}

.checkbox,
.more {
width: 4.8rem;
}
}

@media (max-width: 1440px) {
.table table {
width: 160rem;
}
}
}
</style>
4 changes: 4 additions & 0 deletions @xen-orchestra/lite/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"dns": "DNS",
"do-you-have-needs": "You have needs and/or expectations? Let us know",
"documentation": "Documentation",
"edit": "Edit",
"edit-config": "Edit config",
"enabled": "Enabled",
"error-occurred": "An error has occurred",
Expand Down Expand Up @@ -186,6 +187,7 @@
"new-features-are-coming": "New features are coming soon!",
"news": "News",
"news-name": "{name} news",
"none": "None",
"no-alarm-triggered": "No alarm triggered",
"no-result": "No result",
"no-selected-vm-can-be-exported": "No selected VM can be exported",
Expand All @@ -199,6 +201,7 @@
"password": "Password",
"password-invalid": "Password invalid",
"pause": "Pause",
"pifs": "PIFs",
"please-confirm": "Please confirm",
"pool-cpu-usage": "Pool CPU Usage",
"pool-ram-usage": "Pool RAM Usage",
Expand All @@ -220,6 +223,7 @@

"resume": "Resume",
"save": "Save",
"scan-pifs": "Scan PIFs",
"select-compression": "Select a compression",
"select-destination-host": "Select a destination host",
"selected-vms-in-execution": "Some selected VMs are running",
Expand Down
4 changes: 4 additions & 0 deletions @xen-orchestra/lite/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"display": "Affichage",
"do-you-have-needs": "Vous avez des besoins et/ou des attentes ? Faites le nous savoir",
"documentation": "Documentation",
"edit": "Modifier",
"edit-config": "Modifier config",
"enabled": "Activé",
"error-occurred": "Une erreur est survenue",
Expand Down Expand Up @@ -186,6 +187,7 @@
"new-features-are-coming": "De nouvelles fonctionnalités arrivent bientôt !",
"news": "Actualités",
"news-name": "Actualités {name}",
"none": "Aucun",
"no-alarm-triggered": "Aucune alarme déclenchée",
"no-result": "Aucun résultat",
"no-selected-vm-can-be-exported": "Aucune VM sélectionnée ne peut être exportée",
Expand All @@ -199,6 +201,7 @@
"password": "Mot de passe",
"password-invalid": "Mot de passe incorrect",
"pause": "Pause",
"pifs": "PIFs",
"please-confirm": "Veuillez confirmer",
"pool-cpu-usage": "Utilisation CPU du Pool",
"pool-ram-usage": "Utilisation RAM du Pool",
Expand All @@ -220,6 +223,7 @@

"resume": "Reprendre",
"save": "Enregistrer",
"scan-pifs": "Scanner les PIFs",
"select-compression": "Sélectionnez une compression",
"select-destination-host": "Sélectionnez un hôte de destination",
"selected-vms-in-execution": "Certaines VMs sélectionnées sont en cours d'exécution",
Expand Down
20 changes: 20 additions & 0 deletions @xen-orchestra/lite/src/stores/xen-api/pif-metrics.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { XenApiPif } from '@/libs/xen-api/xen-api.types'
import { createXapiStoreConfig } from '@/stores/xen-api/create-xapi-store-config'
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
import { defineStore } from 'pinia'

export const usePifMetricsStore = defineStore('xen-api-pif-metrics', () => {
const { context: baseContext, ...configRest } = createXapiStoreConfig('pif_metrics')

const getPifCarrier = (pif: XenApiPif) => {
const pifMetrics = baseContext.getByOpaqueRef(pif.metrics)
return pifMetrics.carrier
}

const context = {
...baseContext,
getPifCarrier,
}

return createSubscribableStoreContext({ context, ...configRest }, {})
})
34 changes: 34 additions & 0 deletions @xen-orchestra/lite/src/stores/xen-api/pif.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import { createXapiStoreConfig } from '@/stores/xen-api/create-xapi-store-config'
import { useHostStore } from '@/stores/xen-api/host.store'
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
import { defineStore } from 'pinia'
import { computed } from 'vue'
import { useRoute } from 'vue-router'

export const usePifStore = defineStore('xen-api-pif', () => {
const route = useRoute()

const deps = {
hostStore: useHostStore(),
}
const { context: baseContext, ...configRest } = createXapiStoreConfig('pif')

const hostContext = deps.hostStore.getContext()

const currentHostPifs = computed(() => {
const currentHostUuid = route.params.uuid as XenApiHost['uuid']

return baseContext.records.value.filter(pif => {
const host = hostContext.getByOpaqueRef(pif.host)
return host?.uuid === currentHostUuid
})
})

const context = {
...baseContext,
currentHostPifs,
}

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