feat(web-core): adapt card rows to their content in side panels (#10733)

* feat(web-core): adapt card rows to their content in side panels

- Key and value stay on one line when they fit, the value moves below the key otherwise
- New `values` prop on `VtsCardRowKeyValue`: one copy button per value, a "Copy all"
  action and an optional "see more" limit for fields with multiple values
- New `VtsCardRowList` component holding the 0.8rem spacing between rows
- `UiInfo` text is small inside side panels (icon stays medium), medium elsewhere
- Side panel links use the small size

* docs: add changelog entries
This commit is contained in:
Joris K
2026-11-06 18:38:09 +01:00
committed by GitHub
parent 2e586f0369
commit 43a7e3d3ac
71 changed files with 655 additions and 994 deletions

View File

@@ -3,6 +3,7 @@
## **next**
- [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, bond devices) 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))
## **0.27.0** (2026-10-29)

View File

@@ -12,7 +12,7 @@
<!-- PIF -->
<UiPanelCard>
<UiPanelCardTitle :id="pif.uuid" size="medium" :label="isBond ? t('bond') : t('pif')" />
<div class="content">
<VtsCardRowList>
<!-- NETWORK -->
<VtsCardRowKeyValue>
<template #key>
@@ -82,34 +82,15 @@
</UiTagsList>
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
<!-- NETWORK INFORMATION -->
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('network-information')" />
<div class="content">
<VtsCardRowList>
<!-- IP ADDRESSES -->
<div v-if="ipAddresses.length">
<VtsCardRowKeyValue v-for="(ip, index) in ipAddresses" :key="ip">
<template #key>
<div v-if="index === 0">{{ t('ip-addresses') }}</div>
</template>
<template #value>
<span v-tooltip class="text-ellipsis">{{ ip }}</span>
</template>
<template #addons>
<VtsCopyButton :value="ip" />
<VtsCopyAllMenuItem v-if="index === 0 && ipAddresses.length > 1" :values="ipAddresses" />
</template>
</VtsCardRowKeyValue>
</div>
<VtsCardRowKeyValue v-else>
<template #key>
{{ t('ip-addresses') }}
</template>
<template #value>
<span class="value" />
</template>
<VtsCardRowKeyValue :values="ipAddresses">
<template #key>{{ t('ip-addresses') }}</template>
</VtsCardRowKeyValue>
<!-- MAC ADDRESSES -->
<VtsCardRowKeyValue>
@@ -173,26 +154,15 @@
</template>
</VtsCardRowKeyValue>
<!-- BOND DEVICES -->
<div>
<VtsCardRowKeyValue v-for="(device, index) in bondDevices" :key="device">
<template #key>
<div v-if="index === 0">{{ t('bond-devices') }}</div>
</template>
<template #value>
<span v-tooltip class="text-ellipsis">{{ device }}</span>
</template>
<template v-if="device" #addons>
<VtsCopyButton :value="device" />
<VtsCopyAllMenuItem v-if="index === 0 && bondDevices.length > 1" :values="bondDevicesFiltered" />
</template>
</VtsCardRowKeyValue>
</div>
</div>
<VtsCardRowKeyValue v-if="bondDevicesFiltered.length > 0" :values="bondDevicesFiltered">
<template #key>{{ t('bond-devices') }}</template>
</VtsCardRowKeyValue>
</VtsCardRowList>
</UiPanelCard>
<!-- PROPERTIES -->
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('properties')" />
<div class="content">
<VtsCardRowList>
<!-- MTU -->
<VtsCardRowKeyValue>
<template #key>
@@ -226,7 +196,7 @@
<VtsCopyButton :value="networkPurpose" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
</VtsSidePanel>
@@ -239,7 +209,7 @@ import { useNetworkStore } from '@/stores/xen-api/network.store.ts'
import { usePifMetricsStore } from '@/stores/xen-api/pif-metrics.store.ts'
import { usePifStore } from '@/stores/xen-api/pif.store.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import VtsForgetButton from '@core/components/forget-button/VtsForgetButton.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
@@ -331,12 +301,6 @@ const speed = computed(() => {
<style scoped lang="postcss">
.host-pif-side-panel {
.content {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.network {
display: flex;
gap: 0.8rem;

View File

@@ -21,7 +21,7 @@
<template v-if="network" #default>
<UiPanelCard>
<UiPanelCardTitle :id="network.uuid" size="medium" :label="network.name_label" />
<div class="content">
<VtsCardRowList>
<!-- DESCRIPTION -->
<VtsCardRowKeyValue truncate align-top>
<template #key>{{ t('description') }}</template>
@@ -63,7 +63,7 @@
<template #key>{{ t('locking-mode-default') }}</template>
<template #value>{{ networkDefaultLockingMode }}</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
<UiPanelCard v-if="pifsCount && pifsCount > 0">
<UiPanelCardTitle size="medium" :label="t('pifs')" :counter="pifsCount" />
@@ -97,6 +97,7 @@ import type { XenApiNetwork } from '@/libs/xen-api/xen-api.types.ts'
import { useNetworkDelete } from '@/modules/network/composables/use-network-delete.composable.ts'
import { usePifStore } from '@/stores/xen-api/pif.store.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'
import VtsDeleteButton from '@core/components/delete-button/VtsDeleteButton.vue'
import MenuItem from '@core/components/menu/MenuItem.vue'
@@ -149,12 +150,6 @@ const { copy, copied, isSupported: isClipboardSupported } = useClipboard({ sourc
<style scoped lang="postcss">
.pool-network-side-panel {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.text-left {
text-align: left;
}

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="host-hardware-specifications-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('hardware-specifications')" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('manufacturer-info') }}</template>
<template #value>{{ manufacturerInfo }}</template>
@@ -16,7 +16,7 @@
<VtsCopyButton :value="coreSocketInfo" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -24,6 +24,7 @@
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { getHostCoreSocketInfo, getHostManufacturerInfo } from '@/modules/host/utils/host.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'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -40,13 +41,3 @@ const manufacturerInfo = computed(() => getHostManufacturerInfo(host))
const coreSocketInfo = computed(() => getHostCoreSocketInfo(host))
</script>
<style lang="postcss" scoped>
.host-hardware-specifications-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -7,7 +7,7 @@
:icon="hostIcon"
:to="{ name: '/host/[uuid]/dashboard', params: { uuid: host.uuid } }"
/>
<div class="content">
<VtsCardRowList class="content">
<VtsCardRowKeyValue>
<template #key>{{ t('state') }}</template>
<template #value>
@@ -98,7 +98,7 @@
<VtsCopyButton :value="noMissingPatches ? t('patches-up-to-date') : t('n-missing', nMissingPatches)" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -110,6 +110,7 @@ import { getHostIcon } from '@/modules/host/utils/host.util.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.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'
import VtsEnabledState from '@core/components/enabled-state/VtsEnabledState.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
@@ -146,10 +147,6 @@ const noMissingPatches = computed(() => nMissingPatches.value === 0)
<style lang="postcss" scoped>
.host-infos-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
.value {
display: flex;
align-items: center;

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="host-network-card">
<UiPanelCard>
<UiPanelCardTitle :label="t('network')" size="medium" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('management-ip') }}</template>
<template #value>{{ host.address }}</template>
@@ -9,37 +9,13 @@
<VtsCopyButton :value="host.address" />
</template>
</VtsCardRowKeyValue>
<template v-if="ipV4Addresses.length > 0">
<VtsCardRowKeyValue v-for="(ip, index) in ipV4Addresses" :key="ip">
<template #key>
<div v-if="index === 0">{{ t('ipv4-addresses') }}</div>
</template>
<template #value>{{ ip }}</template>
<template #addons>
<VtsCopyButton :value="ip" />
<VtsCopyAllMenuItem v-if="index === 0 && ipV4Addresses.length > 1" :values="ipV4Addresses" />
</template>
</VtsCardRowKeyValue>
</template>
<VtsCardRowKeyValue v-else>
<VtsCardRowKeyValue :values="ipV4Addresses">
<template #key>{{ t('ipv4-addresses') }}</template>
</VtsCardRowKeyValue>
<template v-if="ipV6Addresses.length > 0">
<VtsCardRowKeyValue v-for="(ip, index) in ipV6Addresses" :key="ip">
<template #key>
<div v-if="index === 0">{{ t('ipv6-addresses') }}</div>
</template>
<template #value>{{ ip }}</template>
<template #addons>
<VtsCopyButton :value="ip" />
<VtsCopyAllMenuItem v-if="index === 0 && ipV6Addresses.length > 1" :values="ipV6Addresses" />
</template>
</VtsCardRowKeyValue>
</template>
<VtsCardRowKeyValue v-else>
<VtsCardRowKeyValue :values="ipV6Addresses">
<template #key>{{ t('ipv6-addresses') }}</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -47,7 +23,7 @@
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { usePifStore } from '@/stores/xen-api/pif.store.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -66,13 +42,3 @@ const pifs = computed(() => pifsByHost.value.get(host.$ref))
const ipV4Addresses = computed(() => pifs.value?.map(pif => pif.IP).filter(ip => ip) ?? [])
const ipV6Addresses = computed(() => pifs.value?.flatMap(pif => pif.IPv6.filter(ip => ip)) ?? [])
</script>
<style lang="postcss" scoped>
.host-network-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="host-software-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('software')" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('version') }}</template>
<template #value>{{ host.software_version.product_version }}</template>
@@ -9,13 +9,14 @@
<VtsCopyButton :value="host.software_version.product_version" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.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'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -27,13 +28,3 @@ defineProps<{
const { t } = useI18n()
</script>
<style lang="postcss" scoped>
.host-software-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,12 +1,12 @@
<template>
<UiPanelCard class="storage-repository-infos-card">
<UiPanelCard>
<UiPanelCardTitle
:id="sr.uuid"
size="medium"
:label="sr.name_label"
:icon="sr.name_label ? srStatusIcon : undefined"
/>
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('status') }}</template>
<template #value>
@@ -56,7 +56,7 @@
<template #key>{{ t('high-availability') }}</template>
<template #value><VtsStatus :status="isHaSr" /></template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -66,6 +66,7 @@ import { useSrUtils } from '@/modules/storage-repository/composables/sr-utils.co
import { useSrStore } from '@/stores/xen-api/sr.store.ts'
import type { SrScope } from '@core/types/storage-repository.type.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'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import VtsTag from '@core/components/tag/VtsTag.vue'
@@ -96,13 +97,3 @@ const allocationStrategy = computed(() => getAllocationStrategy(sr) ?? t('unknow
const isHaSr = computed(() => isHaSrForPool(sr, pool))
</script>
<style scoped lang="postcss">
.storage-repository-infos-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -5,10 +5,10 @@
{{ t('no-pbd-attached') }}
</VtsStateHero>
<template v-else>
<div class="content">
<VtsCardRowList class="content">
<VtsStatus :status="srConnectionStatus" />
</div>
<div v-if="areSomePbdsDisconnected" class="content">
</VtsCardRowList>
<VtsCardRowList v-if="areSomePbdsDisconnected" class="content">
<template v-for="(pbd, index) in disconnectedPbds" :key="pbd.uuid">
<VtsDivider v-if="index > 0" class="divider" type="stretch" />
<UiPanelCardTitle size="small" :label="t('disconnected-pbd-number', { n: index + 1 })" class="subtitle" />
@@ -31,7 +31,7 @@
accent="info"
/>
</template>
</div>
</VtsCardRowList>
</template>
</UiPanelCard>
</template>
@@ -43,6 +43,7 @@ import { usePbdUtils } from '@/modules/storage-repository/composables/pbd-utils.
import { useSrUtils } from '@/modules/storage-repository/composables/sr-utils.composable.ts'
import type { SrScope } from '@core/types/storage-repository.type.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsDivider from '@core/components/divider/VtsDivider.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
@@ -70,10 +71,6 @@ const { areSomePbdsDisconnected, disconnectedPbds } = usePbdUtils(pbdsInScope)
<style scoped lang="postcss">
.storage-repository-pbds-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
.subtitle {
margin-block-end: 0.4rem;
}

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="card-container">
<UiPanelCardTitle size="medium" :label="t('configuration')" />
<div class="content">
<VtsCardRowList>
<VdiFormatCardItem :format="vdi.sm_config['image-format']" />
<VtsCardRowKeyValue>
<template #key>
@@ -43,7 +43,7 @@
<VtsStatus :status="isBootable" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -53,6 +53,7 @@ import type { XenApiVbd, XenApiVdi } from '@/libs/xen-api/xen-api.types.ts'
import VdiFormatCardItem from '@/modules/vdi/components/list/panel/card-items/VdiFormatCardItem.vue'
import { useSrStore } from '@/stores/xen-api/sr.store.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'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
@@ -73,11 +74,6 @@ const isBootable = computed(() => vbd?.bootable ?? false)
<style scoped lang="postcss">
.card-container {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.storage {
display: flex;
gap: 0.8rem;

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="card-container">
<UiPanelCard>
<UiPanelCardTitle :id="vdi.uuid" size="medium" :label="vdi.name_label" :icon="vdiIcon" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue truncate align-top>
<template #key>{{ t('description') }}</template>
<template #value>{{ vdi.name_description }}</template>
@@ -36,7 +36,7 @@
<VtsCopyButton :value="vbd.device" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -45,6 +45,7 @@ import type { XenApiVbd, XenApiVdi } from '@/libs/xen-api/xen-api.types.ts'
import { useVbdsStatus, type VbdAttachmentStatus } from '@/modules/vbd/composables/use-vbds-status.composable.ts'
import type { IconName } from '@core/icons'
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'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import VtsTag from '@core/components/tag/VtsTag.vue'
@@ -79,13 +80,3 @@ const vbdsStatus = useMapper<VbdAttachmentStatus, (typeof CONNECTION_STATUS)[key
'noneAttached'
)
</script>
<style scoped lang="postcss">
.card-container {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -8,7 +8,7 @@
<!-- VIF -->
<UiPanelCard>
<UiPanelCardTitle :id="vif.uuid" size="medium" :label="t('vif')" />
<div class="content">
<VtsCardRowList>
<!-- NETWORK -->
<VtsCardRowKeyValue>
<template #key>
@@ -79,32 +79,15 @@
{{ isTxChecksummingEnabled(vif) }}
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
<!-- VIF NETWORK INFORMATION -->
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('network-information')" />
<div class="content">
<VtsCardRowList>
<!-- IP ADDRESSES -->
<div v-if="ipAddresses.length">
<VtsCardRowKeyValue v-for="(ip, index) in ipAddresses" :key="ip">
<template #key>
<div v-if="index === 0">{{ t('ip-addresses') }}</div>
</template>
<template #value>{{ ip }}</template>
<template #addons>
<VtsCopyButton :value="ip" />
<VtsCopyAllMenuItem v-if="index === 0 && ipAddresses.length > 1" :values="ipAddresses" />
</template>
</VtsCardRowKeyValue>
</div>
<VtsCardRowKeyValue v-else>
<template #key>
{{ t('ip-addresses') }}
</template>
<template #value>
<span class="value" />
</template>
<VtsCardRowKeyValue :values="ipAddresses">
<template #key>{{ t('ip-addresses') }}</template>
</VtsCardRowKeyValue>
<!-- MAC ADDRESSES -->
<VtsCardRowKeyValue>
@@ -118,7 +101,7 @@
<VtsCopyButton :value="vif.MAC" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
</VtsSidePanel>
@@ -133,7 +116,7 @@ import { useNetworkStore } from '@/stores/xen-api/network.store.ts'
import { useVmGuestMetricsStore } from '@/stores/xen-api/vm-guest-metrics.store.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
@@ -187,12 +170,6 @@ const rateLimit = computed(() => {
<style scoped lang="postcss">
.vm-vifs-side-panel {
.content {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.value:empty::before {
content: '-';
}

View File

@@ -317,6 +317,13 @@ declare module 'vue-router/auto-routes' {
Record<never, never>,
| never
>,
'/story/web-core/card/vts-card-row-list': RouteRecordInfo<
'/story/web-core/card/vts-card-row-list',
'/story/web-core/card/vts-card-row-list',
Record<never, never>,
Record<never, never>,
| never
>,
'/story/web-core/cell-object/vts-cell-object': RouteRecordInfo<
'/story/web-core/cell-object/vts-cell-object',
'/story/web-core/cell-object/vts-cell-object',
@@ -1385,6 +1392,12 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
'src/stories/web-core/card/vts-card-row-list.story.vue': {
routes:
| '/story/web-core/card/vts-card-row-list'
views:
| never
}
'src/stories/web-core/cell-object/vts-cell-object.story.vue': {
routes:
| '/story/web-core/cell-object/vts-cell-object'

View File

@@ -3,7 +3,7 @@
v-slot="{ properties, settings }"
:params="[
slot('key').help('Meant to receive a title'),
slot('value').help('Meant to receive text, tags or other value'),
slot('value').help('Meant to receive text, tags or other value. Overrides the value rendered from `values`'),
slot('addons').help('Meant to receive icons or other actions buttons '),
prop('truncate')
.type('boolean | number')
@@ -21,6 +21,16 @@
.help('Whether to align the key on the first line in case of multiline value')
.type('boolean')
.widget(),
prop('values')
.help('Values rendered as text with a copy button each, plus a copy all action when there are several')
.type('string[]')
.widget(object())
.preset(['192.168.1.10', '192.168.1.11', '10.0.0.12', 'fe80::a00:27ff:fe4e:66a1']),
prop('maxVisibleValues')
.help('Number of values shown before a see more button')
.type('number')
.widget()
.preset(3),
setting('valueSlotDescription')
.preset(
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Velit facilis pariatur maiores officia fugit odit, doloremque obcaecati quisquam distinctio adipisci fugiat incidunt hic rerum? Ullam, natus. Iste eum porro dolorum!'
@@ -29,12 +39,16 @@
]"
>
<UiCard class="card">
<VtsCardRowKeyValue v-bind="properties">
<VtsCardRowKeyValue :values="properties.values" :max-visible-values="properties.maxVisibleValues">
<template #key>IP addresses</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue v-bind="{ ...properties, values: undefined }">
<template #key>Description</template>
<template #value>{{ settings.valueSlotDescription }}</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue v-bind="properties">
<VtsCardRowKeyValue v-bind="{ ...properties, values: undefined }">
<template #key>Network</template>
<template #value>
<UiObjectLink route="`/vm/test/console`" icon="object:network:connected">
@@ -46,7 +60,7 @@
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue v-bind="properties">
<VtsCardRowKeyValue v-bind="{ ...properties, values: undefined }">
<template #key>Status</template>
<template #value>
<UiInfo accent="success">Connected</UiInfo>
@@ -57,7 +71,7 @@
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue v-bind="{ ...properties, truncate: false }">
<VtsCardRowKeyValue v-bind="{ ...properties, truncate: false, values: undefined }">
<template #key>Tags</template>
<template #value>
<UiTagsList v-if="tags.length > 0">
@@ -75,7 +89,7 @@
<script lang="ts" setup>
import ComponentStory from '@/components/component-story/ComponentStory.vue'
import { prop, setting, slot } from '@/libs/story/story-param.ts'
import { choice, text } from '@/libs/story/story-widget.ts'
import { choice, object, text } from '@/libs/story/story-widget.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'

View File

@@ -0,0 +1,35 @@
<template>
<ComponentStory :params="[slot().help('Meant to receive VtsCardRowKeyValue components')]">
<UiCard class="card">
<VtsCardRowList>
<VtsCardRowKeyValue :values="['10.0.0.12']">
<template #key>IP address</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue :values="['1500']">
<template #key>MTU</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue>
<template #key>Status</template>
<template #value>
<UiInfo accent="success">Connected</UiInfo>
</template>
</VtsCardRowKeyValue>
</VtsCardRowList>
</UiCard>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from '@/components/component-story/ComponentStory.vue'
import { slot } from '@/libs/story/story-param.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiInfo from '@core/components/ui/info/UiInfo.vue'
</script>
<style lang="postcss" scoped>
.card {
width: 400px;
}
</style>

View File

@@ -4,7 +4,10 @@
:params="[
prop('accent').enum('info', 'success', 'warning', 'danger', 'muted').required().preset('info').widget(),
prop('wrap').bool().help('Choose if the text should wrap if too long').widget(),
prop('size').enum('small', 'medium').preset('medium').widget(),
prop('size')
.enum('small', 'medium')
.help('Text size only, the icon is always medium. Defaults to small inside a VtsSidePanel, medium elsewhere')
.widget(),
slot(),
setting('defaultSlot').widget(text()).preset('message'),
]"

View File

@@ -0,0 +1,85 @@
<template>
<div class="card-row-value-list">
<div v-for="(value, index) in visibleValues" :key="index" class="value-row">
<div class="value">{{ value }}</div>
<div class="addons">
<VtsCopyButton :value />
<slot v-if="index === 0" name="first-row-addons" />
</div>
</div>
<UiButton
v-if="hiddenValuesCount > 0"
class="see-more"
size="small"
accent="brand"
variant="tertiary"
@click="toggleExpanded()"
>
{{ isExpanded ? t('action:see-less') : t('action:see-n-more', { n: hiddenValuesCount }) }}
</UiButton>
</div>
</template>
<script lang="ts" setup>
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
import { useToggle } from '@vueuse/shared'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { values, maxVisibleValues } = defineProps<{
values: string[]
maxVisibleValues?: number
}>()
defineSlots<{
'first-row-addons'?(): any
}>()
const { t } = useI18n()
const [isExpanded, toggleExpanded] = useToggle(false)
const hiddenValuesCount = computed(() =>
maxVisibleValues === undefined ? 0 : Math.max(values.length - maxVisibleValues, 0)
)
const visibleValues = computed(() => (isExpanded.value ? values : values.slice(0, maxVisibleValues)))
</script>
<style lang="postcss" scoped>
.card-row-value-list {
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: 0.8rem;
.value-row {
display: flex;
align-items: flex-start;
gap: 1.6rem;
}
.value {
flex: 1 1 auto;
min-width: 0;
overflow-wrap: anywhere;
color: var(--color-neutral-txt-primary);
&:empty::before {
content: '-';
}
}
.addons {
display: flex;
gap: 0.8rem;
height: 1lh;
align-items: center;
}
.see-more {
align-self: flex-start;
}
}
</style>

View File

@@ -1,48 +1,67 @@
<template>
<div
class="vts-card-row-key-value typo-body-regular-small"
:class="{ truncate: shouldTruncate, 'align-top': alignTop }"
>
<div class="key">
<slot name="key" />
</div>
<div class="value-container">
<div ref="truncatableValueElement" class="value" :class="{ truncated: shouldTruncate && !isExpanded }">
<slot name="value" />
<div class="vts-card-row-key-value typo-body-regular-small" :class="className">
<div ref="keyValue" class="key-value">
<div class="key">
<slot name="key" />
<div v-if="multipleValues && isStacked" class="addons">
<slot name="addons" />
<VtsCopyAllMenuItem :values="multipleValues" />
</div>
</div>
<CardRowValueList v-if="multipleValues" :values="multipleValues" :max-visible-values>
<template v-if="!isStacked" #first-row-addons>
<slot name="addons" />
<VtsCopyAllMenuItem :values="multipleValues" />
</template>
</CardRowValueList>
<div v-else class="value-container">
<div ref="truncatableValueElement" class="value" :class="{ truncated: shouldTruncate && !isExpanded }">
<slot name="value">
<template v-if="singleValue !== undefined">{{ singleValue }}</template>
</slot>
</div>
</div>
<UiButtonIcon
v-if="shouldTruncate"
v-tooltip="isExpanded ? t('action:show-less') : t('action:show-more')"
:icon="isExpanded ? 'fa:chevron-up' : 'fa:chevron-down'"
class="show-more"
size="small"
accent="brand"
@click="toggleExpanded()"
/>
</div>
<div v-if="slots.addons" class="addons">
<slot name="addons" />
<div v-if="!multipleValues && (slots.addons || singleValue !== undefined || shouldTruncate)" class="actions">
<div v-if="slots.addons || singleValue !== undefined" class="addons">
<slot name="addons" />
<VtsCopyButton v-if="singleValue !== undefined" :value="singleValue" />
</div>
<div v-if="shouldTruncate" class="show-more">
<UiButtonIcon
v-tooltip="isExpanded ? t('action:show-less') : t('action:show-more')"
:icon="isExpanded ? 'fa:chevron-up' : 'fa:chevron-down'"
size="small"
accent="brand"
@click="toggleExpanded()"
/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import CardRowValueList from '@core/components/card/CardRowValueList.vue'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { calculateLineCount } from '@core/utils/line-height.util.ts'
import { useStyleTag } from '@vueuse/core'
import { useResizeObserver, useStyleTag } from '@vueuse/core'
import { useToggle } from '@vueuse/shared'
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { truncate } = defineProps<{
const { truncate, alignTop, values } = defineProps<{
truncate?: number | boolean
alignTop?: boolean
values?: string[]
maxVisibleValues?: number
}>()
const slots = defineSlots<{
key(): any
value(): any
value?(): any
addons?(): any
}>()
@@ -50,6 +69,24 @@ const DEFAULT_MAX_LINES = 5
const { t } = useI18n()
const multipleValues = computed(() => (values !== undefined && values.length > 1 ? values : undefined))
const singleValue = computed(() => (values?.length === 1 ? values[0] : undefined))
const keyValueRef = useTemplateRef('keyValue')
const isStacked = ref(false)
useResizeObserver(
() => (multipleValues.value !== undefined ? keyValueRef.value : undefined),
() => {
const key = keyValueRef.value?.firstElementChild
const valueList = keyValueRef.value?.lastElementChild
isStacked.value =
key instanceof HTMLElement && valueList instanceof HTMLElement && valueList.offsetTop > key.offsetTop
}
)
const truncatableValueElementRef = useTemplateRef('truncatableValueElement')
const lineCount = ref(0)
@@ -67,6 +104,13 @@ const shouldTruncate = computed(() => isTruncationEnabled.value && lineCount.val
const [isExpanded, toggleExpanded] = useToggle(false)
const className = computed(() => ({
truncate: shouldTruncate.value,
'align-top': alignTop,
'multiple-values': multipleValues.value !== undefined,
stacked: isStacked.value,
}))
const calculateLines = async () => {
await nextTick()
@@ -89,42 +133,36 @@ useStyleTag(style)
onMounted(() => calculateLines())
watch(
() => slots.value?.(),
() => calculateLines()
)
watch([() => slots.value?.(), () => values], () => calculateLines())
</script>
<style lang="postcss" scoped>
.vts-card-row-key-value {
display: flex;
align-items: center;
align-items: flex-start;
column-gap: 1.6rem;
&.truncate {
display: grid;
row-gap: 0.4rem;
grid-template-columns: 1fr auto;
.key {
grid-column: 1 / -1;
grid-row: 1;
}
.addons {
grid-column: 2;
grid-row: 1;
}
.value-container {
grid-column: 1 / -1;
}
.key-value {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem 1.6rem;
min-width: 0;
}
&.align-top:not(.truncate) {
&.align-top .key-value,
&.multiple-values .key-value {
align-items: flex-start;
}
&.multiple-values.stacked .key {
display: flex;
flex-grow: 1;
justify-content: space-between;
gap: 0.8rem;
}
.key {
overflow-wrap: break-word;
color: var(--color-neutral-txt-secondary);
@@ -132,15 +170,16 @@ watch(
.value-container {
display: flex;
align-items: end;
gap: 0.8rem;
flex: 1 1 auto;
min-width: 0;
.value {
min-width: 0;
color: var(--color-neutral-txt-primary);
}
.show-more {
flex-shrink: 0;
:slotted(*) {
vertical-align: middle;
}
}
}
@@ -148,12 +187,23 @@ watch(
content: '-';
}
.addons {
.actions {
display: flex;
flex-direction: column;
align-self: stretch;
align-items: flex-end;
}
.addons,
.show-more {
display: flex;
gap: 0.8rem;
margin-left: auto;
font-size: 1.6rem;
height: 1lh;
align-items: center;
}
.show-more {
margin-block-start: auto;
}
}
</style>

View File

@@ -0,0 +1,19 @@
<template>
<div class="vts-card-row-list">
<slot />
</div>
</template>
<script lang="ts" setup>
defineSlots<{
default(): any
}>()
</script>
<style lang="postcss" scoped>
.vts-card-row-list {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
</style>

View File

@@ -49,7 +49,8 @@ import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { PANEL_TRANSITION_DURATION, usePanelStore } from '@core/stores/panel.store.ts'
import { useUiStore } from '@core/stores/ui.store.ts'
import { computed, watch } from 'vue'
import { IK_INFO_SIZE } from '@core/utils/injection-keys.util.ts'
import { computed, provide, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { hasSelection } = defineProps<{
@@ -76,6 +77,8 @@ const showResizeHandle = computed(() => !uiStore.isSmall && panelStore.isExpande
const transformTransition = `transform ${PANEL_TRANSITION_DURATION}ms`
provide(IK_INFO_SIZE, 'small')
watch(
[() => hasSelection, () => panelStore.isLocked, () => uiStore.isSmall],
([hasSelection]) => {

View File

@@ -1,7 +1,7 @@
<!-- v6 -->
<template>
<div class="ui-info">
<VtsIcon class="icon" :name="icon" :size />
<VtsIcon class="icon" :name="icon" size="medium" />
<p v-tooltip="!wrap" class="label" :class="[textSize, { 'text-ellipsis': !wrap }]">
<slot />
</p>
@@ -13,14 +13,16 @@ import VtsIcon from '@core/components/icon/VtsIcon.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import type { IconName } from '@core/icons'
import { useMapper } from '@core/packages/mapper'
import { IK_INFO_SIZE } from '@core/utils/injection-keys.util.ts'
import { inject } from 'vue'
export type InfoAccent = 'info' | 'success' | 'warning' | 'danger' | 'muted'
type Size = 'small' | 'medium'
export type InfoSize = 'small' | 'medium'
const { accent, size = 'medium' } = defineProps<{
const { accent, size } = defineProps<{
accent: InfoAccent
wrap?: boolean
size?: Size
size?: InfoSize
}>()
defineSlots<{
@@ -39,8 +41,10 @@ const icon = useMapper<InfoAccent, IconName>(
'muted'
)
const textSize = useMapper<Size, any>(
() => size,
const contextSize = inject(IK_INFO_SIZE, undefined)
const textSize = useMapper<InfoSize, any>(
() => size ?? contextSize ?? 'medium',
{
small: 'typo-body-regular-small',
medium: 'typo-body-regular',

View File

@@ -1,4 +1,5 @@
import type { InputWrapperController } from '@core/components/input-wrapper/VtsInputWrapper.vue'
import type { InfoSize } from '@core/components/ui/info/UiInfo.vue'
import type { ModalAccent } from '@core/components/ui/modal/UiModal.vue'
import type { ValueFormatter } from '@core/types/chart.ts'
import type { ComputedRef, InjectionKey, Ref } from 'vue'
@@ -24,3 +25,5 @@ export const IK_DISABLED = Symbol('IK_DISABLED') as InjectionKey<ComputedRef<boo
export const IK_INPUT_WRAPPER_CONTROLLER = Symbol('IK_INPUT_WRAPPER_CONTROLLER') as InjectionKey<InputWrapperController>
export const IK_OVERLAY_ACCENT = Symbol('IK_OVERLAY_ACCENT') as InjectionKey<ComputedRef<ModalAccent>>
export const IK_INFO_SIZE = Symbol('IK_INFO_SIZE') as InjectionKey<InfoSize>

View File

@@ -63,12 +63,13 @@ Prefer this shape: one assertion covering every row a user sees, and it fails lo
`src/test/find-rendered-values.ts` does exactly that reduction, so a card test does not re-roll it:
| Helper | Reads |
| ------------------------ | ---------------------------------------------------------- |
| `findLabelledValues` | every `VtsTabularKeyValueRow` / `VtsKeyValueRow` of a card |
| `findCardLabelledValues` | every `VtsCardRowKeyValue` of a side-panel card |
| `findCopiedValues` | what each copyable key/value row copies, keyed by label |
| `findCardCopiedValues` | what each copyable `VtsCardRowKeyValue` copies, in order |
| Helper | Reads |
| ---------------------------- | ------------------------------------------------------------------ |
| `findLabelledValues` | every `VtsTabularKeyValueRow` / `VtsKeyValueRow` of a card |
| `findCardLabelledValues` | every `VtsCardRowKeyValue` of a side-panel card |
| `findCardLabelledValueLists` | every `VtsCardRowKeyValue`, each value of a multi-value row listed |
| `findCopiedValues` | what each copyable key/value row copies, keyed by label |
| `findCardCopiedValues` | what each copyable `VtsCardRowKeyValue` copies, in order |
```typescript
expect(findLabelledValues(wrapper)).toEqual({ [t('vga')]: t('disabled'), [t('video-ram')]: '8 B' })
@@ -80,13 +81,10 @@ A row offers a copy button through its `copy` prop (copies its `value`) or its `
expect(findCopiedValues(wrapper)).toEqual({ [t('ip-address')]: '10.0.0.1', [t('uuid')]: 'vm-42' })
```
Both collapse rows that share a label into one entry — a list repeating the same label (an address list labelling only its first row) is queried directly instead:
Both read one value per row. A `VtsCardRowKeyValue` given several `values` (an address list, report recipients) renders them all under a single label, so read it with `findCardLabelledValueLists` instead:
```typescript
expect(wrapper.findAll('.vts-card-row-key-value').map(row => row.get('.value').text())).toEqual([
'10.0.0.1',
'10.0.0.2',
])
expect(findCardLabelledValueLists(wrapper)).toEqual({ [t('ip-addresses')]: ['10.0.0.1', '10.0.0.2'] })
```
When a card mixes deterministic values with environment-derived ones (a relative start time, a formatted date), pin the deterministic rows with `toMatchObject` and cover the composition separately by asserting the **list of labels, in order** — the formatted values themselves belong in the test of the composable that derives them.

View File

@@ -12,7 +12,7 @@
</VtsCardRowKeyValue>
<template v-if="isSmartModeEnabled">
<VtsDivider class="divider" type="stretch" />
<div class="content">
<VtsCardRowList class="content">
<VtsCardRowKeyValue v-if="smartModePowerState !== undefined">
<template #key>
{{ t('power-state') }}
@@ -42,7 +42,7 @@
<BackupJobSmartModeTagsItem :tags="smartModeTags.included" :label="t('vms-tags')" />
<BackupJobSmartModeTagsItem :tags="smartModeTags.excluded" :label="t('excluded-vms-tags')" />
</template>
</div>
</VtsCardRowList>
</template>
<template v-if="backedUpVmsCount > 0">
<VtsDivider class="divider" type="stretch" />
@@ -68,6 +68,7 @@ import BackupJobSmartModeTagsItem from '@/modules/backup-job/components/list/pan
import { useXoBackedUpVmsUtils } from '@/modules/backup-job/composables/use-xo-backed-up-vms-utils.composable.ts'
import type { FrontXoVmBackupJob } from '@/modules/backup-job/remote-resources/use-xo-backup-job-collection.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsDivider from '@core/components/divider/VtsDivider.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
@@ -95,10 +96,6 @@ const { backedUpVms, backedUpVmsCount, smartModePools, smartModePowerState, smar
}
.content {
display: flex;
flex-direction: column;
gap: 1.6rem;
.smart-mode-power-state-value {
display: flex;
align-items: center;

View File

@@ -9,7 +9,7 @@ import type {
import type { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
import { createMetadataBackupJob, createVmBackupJob } from '@/test/create-backup-job.ts'
import { createSr } from '@/test/create-sr.ts'
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { findCardLabelledValueLists, findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
@@ -95,7 +95,7 @@ it('lists every type of the backup job, offering to copy them all', () => {
const wrapper = mountInfosCard(backupJob)
const typeLabels = [t('backup:rolling-snapshot'), t('backup:vms-backup'), t('backup:vms-replication')]
expect(findValues(wrapper).slice(0, 3)).toEqual(typeLabels)
expect(findCardLabelledValueLists(wrapper)[t('type')]).toEqual(typeLabels)
expect(wrapper.getComponent(VtsCopyAllMenuItem).props('values')).toEqual(typeLabels)
})

View File

@@ -1,5 +1,5 @@
<template>
<UiPanelCard class="backup-job-infos-card">
<UiPanelCard>
<UiPanelCardTitle
:id="backupJob.id"
size="medium"
@@ -7,16 +7,9 @@
:href="jobOverviewHref"
icon="object:backup-job"
/>
<div class="content">
<VtsCardRowKeyValue v-for="(typeLabel, index) in typeLabels" :key="typeLabel">
<template #key>
<template v-if="index === 0">{{ t('type') }}</template>
</template>
<template #value>{{ typeLabel }}</template>
<template #addons>
<VtsCopyButton :value="typeLabel" />
<VtsCopyAllMenuItem v-if="index === 0 && typeLabels.length > 1" :values="typeLabels" />
</template>
<VtsCardRowList>
<VtsCardRowKeyValue :values="typeLabels">
<template #key>{{ t('type') }}</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue>
<template #key>{{ t('mode') }}</template>
@@ -58,7 +51,7 @@
<template #value />
</VtsCardRowKeyValue>
</template>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -74,7 +67,7 @@ import { useXoSrUtils } from '@/modules/storage-repository/composables/xo-sr-uti
import { useXoSrCollection } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
@@ -111,13 +104,3 @@ const healthCheckSrs = computed(() =>
})
)
</script>
<style scoped lang="postcss">
.backup-job-infos-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,14 +1,14 @@
<template>
<UiPanelCard class="backup-job-logs-card">
<UiPanelCardTitle size="medium" :label="t('last-n-runs', { n: BACKUP_JOB_LAST_RUNS_COUNT })" />
<div class="content">
<VtsCardRowList class="content">
<template v-for="index in BACKUP_JOB_LAST_RUNS_COUNT" :key="index">
<VtsDivider v-if="index > 1" class="divider" type="stretch" />
<UiPanelCardTitle size="small" :label="t('last-run-number', { n: index })" />
<BackupJobRunItem v-if="backupLogs[index - 1]" :backup-run="backupLogs[index - 1]" />
<span v-else class="no-run typo-body-regular-small">{{ t('no-run') }}</span>
</template>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -16,6 +16,7 @@
import BackupJobRunItem from '@/modules/backup-job/components/list/panel/card-items/BackupJobRunItem.vue'
import { BACKUP_JOB_LAST_RUNS_COUNT } from '@/modules/backup-job/utils/xo-backup-job.util.ts'
import type { FrontXoBackupLog } from '@/modules/backup-log/remote-resources/use-xo-backup-log-collection.ts'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsDivider from '@core/components/divider/VtsDivider.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -31,10 +32,6 @@ const { t } = useI18n()
<style scoped lang="postcss">
.backup-job-logs-card {
.content {
display: flex;
flex-direction: column;
gap: 0.6rem;
.divider {
margin-block: 1.6rem;
}

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="backup-job-schedules-card">
<UiPanelCardTitle size="medium" :label="t('schedules')" :counter="schedules.length" />
<div class="content">
<VtsCardRowList class="content">
<template v-for="({ schedule, nextRun }, index) in schedulesWithNextRun" :key="schedule.id">
<VtsDivider v-if="index > 0" class="divider" type="stretch" />
<VtsCardRowKeyValue>
@@ -45,7 +45,7 @@
</template>
</VtsCardRowKeyValue>
</template>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -54,6 +54,7 @@ import type { FrontXoSchedule } from '@/modules/schedule/remote-resources/use-xo
import { getScheduleNextRun } from '@/modules/schedule/utils/xo-schedule.util.ts'
import { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.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'
import VtsDivider from '@core/components/divider/VtsDivider.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
@@ -89,10 +90,6 @@ const schedulesWithNextRun = computed(() =>
<style scoped lang="postcss">
.backup-job-schedules-card {
.content {
display: flex;
flex-direction: column;
gap: 0.6rem;
.divider {
margin-block: 1.6rem;
}

View File

@@ -2,7 +2,7 @@ import BackupJobSettingsCard from '@/modules/backup-job/components/list/panel/ca
import type { FrontAnyXoBackupJob } from '@/modules/backup-job/remote-resources/use-xo-backup-job-collection.ts'
import type { FrontXoProxy, useXoProxyCollection } from '@/modules/proxy/remote-resources/use-xo-proxy-collection.ts'
import { createMetadataBackupJob, createMirrorBackupJob, createVmBackupJob } from '@/test/create-backup-job.ts'
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { findCardLabelledValueLists, findCardLabelledValues } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
@@ -48,16 +48,6 @@ function findLabels(wrapper: ReturnType<typeof mountSettingsCard>) {
return Object.keys(findCardLabelledValues(wrapper))
}
function findRecipients(wrapper: ReturnType<typeof mountSettingsCard>) {
const rows = wrapper.findAll('.vts-card-row-key-value')
const firstRecipientIndex = rows.findIndex(row => row.get('.key').text() === t('report-recipients'))
return rows
.slice(firstRecipientIndex)
.filter((row, index) => index === 0 || row.get('.key').text() === '')
.map(row => row.get('.value').text())
}
describe('rows', () => {
it('shows every setting of a full VM backup job without rolling snapshot', () => {
expect(findLabels(mountSettingsCard(createVmBackupJob({ mode: 'full' })))).toEqual([
@@ -163,7 +153,7 @@ describe('report recipients', () => {
it('shows the first recipients, then all of them on demand', async () => {
const wrapper = mountSettingsCard(createMetadataBackupJob({ settings: { '': { reportRecipients: recipients } } }))
expect(findRecipients(wrapper)).toEqual(recipients.slice(0, 5))
expect(findCardLabelledValueLists(wrapper)[t('report-recipients')]).toEqual(recipients.slice(0, 5))
const seeMoreButton = wrapper.getComponent(UiButton)
@@ -171,7 +161,7 @@ describe('report recipients', () => {
await seeMoreButton.trigger('click')
expect(findRecipients(wrapper)).toEqual(recipients)
expect(findCardLabelledValueLists(wrapper)[t('report-recipients')]).toEqual(recipients)
expect(seeMoreButton.text()).toBe(t('action:see-less'))
})

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="backup-job-settings-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('settings')" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('proxy') }}</template>
<template #value>{{ proxy?.name }}</template>
@@ -39,35 +39,8 @@
<VtsCopyButton :value="reportWhenValueTranslation" />
</template>
</VtsCardRowKeyValue>
<template v-if="reportRecipients.length > 0">
<VtsCardRowKeyValue v-for="(recipient, index) in visibleReportRecipients" :key="recipient" align-top>
<template #key>
<template v-if="index === 0">{{ t('report-recipients') }}</template>
</template>
<template #value>{{ recipient }}</template>
<template #addons>
<VtsCopyButton :value="recipient" />
<VtsCopyAllMenuItem v-if="index === 0 && reportRecipients.length > 1" :values="reportRecipients" />
</template>
</VtsCardRowKeyValue>
<UiButton
v-if="hiddenReportRecipientsCount > 0"
class="see-more"
size="small"
accent="brand"
variant="tertiary"
@click="toggleReportRecipients()"
>
{{
areReportRecipientsExpanded
? t('action:see-less')
: t('action:see-n-more', { n: hiddenReportRecipientsCount })
}}
</UiButton>
</template>
<VtsCardRowKeyValue v-else>
<VtsCardRowKeyValue :values="reportRecipients" :max-visible-values="VISIBLE_REPORT_RECIPIENTS_COUNT" align-top>
<template #key>{{ t('report-recipients') }}</template>
<template #value />
</VtsCardRowKeyValue>
<VtsCardRowKeyValue v-if="hasTransferSettings">
<template #key>{{ t('concurrency') }}</template>
@@ -98,7 +71,7 @@
<VtsStatus :status="!!settings.mergeBackupsSynchronously" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -107,14 +80,12 @@ import { useXoBackupJobSettings } from '@/modules/backup-job/composables/use-xo-
import type { FrontAnyXoBackupJob } from '@/modules/backup-job/remote-resources/use-xo-backup-job-collection.ts'
import { hasBackupJobSnapshotRetention } from '@/modules/backup-job/utils/xo-backup-job.util.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
import UiTag from '@core/components/ui/tag/UiTag.vue'
import { useToggle } from '@vueuse/core'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -122,6 +93,8 @@ const { backupJob } = defineProps<{
backupJob: FrontAnyXoBackupJob
}>()
const VISIBLE_REPORT_RECIPIENTS_COUNT = 5
const { t } = useI18n()
const {
@@ -142,33 +115,5 @@ const canSetCompression = computed(() => backupJob.type === 'backup' && backupJo
const canSetOfflineBackup = computed(() => backupJob.type === 'backup' && !hasBackupJobSnapshotRetention(backupJob))
const VISIBLE_REPORT_RECIPIENTS_COUNT = 5
const [areReportRecipientsExpanded, toggleReportRecipients] = useToggle(false)
const reportRecipients = computed(() => settings.reportRecipients ?? [])
const hiddenReportRecipientsCount = computed(() =>
Math.max(reportRecipients.value.length - VISIBLE_REPORT_RECIPIENTS_COUNT, 0)
)
const visibleReportRecipients = computed(() =>
areReportRecipientsExpanded.value
? reportRecipients.value
: reportRecipients.value.slice(0, VISIBLE_REPORT_RECIPIENTS_COUNT)
)
</script>
<style scoped lang="postcss">
.backup-job-settings-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
.see-more {
align-self: center;
}
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="backup-log-infos-card">
<UiPanelCard>
<UiPanelCardTitle :id="backupLog.id" size="medium" :label="logName" :href="logHref" icon="object:backup-run" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('backup-date') }}</template>
<template #value>{{ formattedStartDate }}</template>
@@ -85,7 +85,7 @@
<VtsStatus :status="backupLog.status" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -99,6 +99,7 @@ import { useXoSrCollection } from '@/modules/storage-repository/remote-resources
import { useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.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'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import UiInfo from '@core/components/ui/info/UiInfo.vue'
@@ -156,13 +157,3 @@ const srs = computed(() =>
})
)
</script>
<style scoped lang="postcss">
.backup-log-infos-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="backup-log-size-and-speed-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('size-and-speed')" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('size') }}</template>
<template #value>{{ size }}</template>
@@ -16,7 +16,7 @@
<VtsCopyButton :value="speed" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -24,6 +24,7 @@
import { useXoBackupLogsUtils } from '@/modules/backup-log/composables/use-xo-backup-log-utils.composable.ts'
import type { FrontXoBackupLog } from '@/modules/backup-log/remote-resources/use-xo-backup-log-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'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -52,13 +53,3 @@ const speed = computed(() => {
return transferSpeed !== undefined ? formatSpeed(transferSpeed) : undefined
})
</script>
<style scoped lang="postcss">
.backup-log-size-and-speed-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<UiPanelCard class="backup-repository-azure-azurite-card">
<UiPanelCard>
<UiPanelCardTitle v-if="azure.type === 'azure'" size="medium" :label="t('azure')" />
<UiPanelCardTitle v-else size="medium" :label="t('azurite')" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('host') }}</template>
<template #value>{{ azure.host }}</template>
@@ -46,13 +46,17 @@
<VtsCopyButton :value="splitPath.subPath" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
<script lang="ts" setup>
import { MASKED_SECRET, splitBackupRepositoryPath } from '@/modules/backup-repository/utils/xo-backup-repository.util.ts'
import {
MASKED_SECRET,
splitBackupRepositoryPath,
} from '@/modules/backup-repository/utils/xo-backup-repository.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'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
@@ -69,13 +73,3 @@ const { t } = useI18n()
const splitPath = computed(() => splitBackupRepositoryPath(azure.path))
</script>
<style scoped lang="postcss">
.backup-repository-azure-azurite-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<UiPanelCard class="backup-repository-infos-card">
<UiPanelCard>
<UiPanelCardTitle
:id="br.id"
:label="br.name"
@@ -7,7 +7,7 @@
:icon="brIcon"
size="medium"
/>
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('status') }}</template>
<template #value>
@@ -44,7 +44,7 @@
<VtsStatus :status="isEncrypted" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -53,6 +53,7 @@ import { useXoBackupRepositoryUtils } from '@/modules/backup-repository/composab
import type { FrontXoBackupRepository } from '@/modules/backup-repository/remote-resources/use-xo-backup-repository-collection.ts'
import { getBackupRepositoryIcon } from '@/modules/backup-repository/utils/xo-backup-repository.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'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
@@ -76,13 +77,3 @@ const { brStatus, brType, brStorageMode, brProxy, isEncrypted } = useXoBackupRep
const brIcon = computed(() => getBackupRepositoryIcon(br, parsedBrUrl?.type))
</script>
<style scoped lang="postcss">
.backup-repository-infos-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="backup-repository-local-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('local')" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('path') }}</template>
<template #value>{{ file.path }}</template>
@@ -9,12 +9,13 @@
<VtsCopyButton :value="file.path" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
<script lang="ts" setup>
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'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -27,13 +28,3 @@ defineProps<{
const { t } = useI18n()
</script>
<style scoped lang="postcss">
.backup-repository-local-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="backup-repository-nfs-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('nfs')" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('host') }}</template>
<template #value>{{ nfs.host }}</template>
@@ -33,13 +33,14 @@
<VtsCopyButton :value="formattedOptions" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
<script lang="ts" setup>
import { formatMountOptions } from '@/modules/backup-repository/utils/xo-backup-repository.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'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -56,13 +57,3 @@ const { t } = useI18n()
const formattedOptions = computed(() => formatMountOptions(options))
</script>
<style scoped lang="postcss">
.backup-repository-nfs-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="backup-repository-s3-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('s3')" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('endpoint-url') }}</template>
<template #value>{{ s3.host }}</template>
@@ -60,13 +60,17 @@
<VtsCopyButton :value="splitPath.subPath" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
<script lang="ts" setup>
import { MASKED_SECRET, splitBackupRepositoryPath } from '@/modules/backup-repository/utils/xo-backup-repository.util.ts'
import {
MASKED_SECRET,
splitBackupRepositoryPath,
} from '@/modules/backup-repository/utils/xo-backup-repository.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'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
@@ -89,13 +93,3 @@ const truncatedAccessKeyId = computed(() =>
s3.username.length > ACCESS_KEY_ID_LIMIT ? `${s3.username.slice(0, ACCESS_KEY_ID_LIMIT)}…` : s3.username
)
</script>
<style scoped lang="postcss">
.backup-repository-s3-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="backup-repository-smb-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('smb')" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('path-on-share') }}</template>
<template #value>{{ pathOnShare }}</template>
@@ -38,7 +38,7 @@
<VtsCopyButton :value="formattedOptions" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -49,6 +49,7 @@ import {
MASKED_SECRET,
} from '@/modules/backup-repository/utils/xo-backup-repository.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'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -67,13 +68,3 @@ const pathOnShare = computed(() => formatSmbPathOnShare(smb.host, smb.path))
const formattedOptions = computed(() => formatMountOptions(options))
</script>
<style scoped lang="postcss">
.backup-repository-smb-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,12 +1,12 @@
<template>
<UiPanelCard class="backup-repository-speed-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('speed')">
<template #more-actions>
<BackupRepositoryBenchmarkButton :br />
</template>
</UiPanelCardTitle>
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('writing-speed') }}</template>
<template #value>{{ writeSpeed }}</template>
@@ -21,7 +21,7 @@
<VtsCopyButton :value="readSpeed" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -30,6 +30,7 @@ import BackupRepositoryBenchmarkButton from '@/modules/backup-repository/compone
import { useXoBackupRepositoryBenchmark } from '@/modules/backup-repository/composables/use-xo-backup-repository-benchmark.composable.ts'
import type { FrontXoBackupRepository } from '@/modules/backup-repository/remote-resources/use-xo-backup-repository-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'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -43,13 +44,3 @@ const { t } = useI18n()
const { writeSpeed, readSpeed } = useXoBackupRepositoryBenchmark(() => br)
</script>
<style scoped lang="postcss">
.backup-repository-speed-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="host-hardware-specifications-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('hardware-specifications')" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('manufacturer-info') }}</template>
<template #value>{{ manufacturerInfo }}</template>
@@ -16,7 +16,7 @@
<VtsCopyButton :value="coreSocketInfo" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -24,6 +24,7 @@
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { getHostCoreSocketInfo, getHostManufacturerInfo } from '@/modules/host/utils/xo-host.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'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -40,13 +41,3 @@ const manufacturerInfo = computed(() => getHostManufacturerInfo(host))
const coreSocketInfo = computed(() => getHostCoreSocketInfo(host))
</script>
<style scoped lang="postcss">
.host-hardware-specifications-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -7,7 +7,7 @@
:to="{ name: '/host/[id]/dashboard', params: { id: host.id } }"
size="medium"
/>
<div class="content">
<VtsCardRowList class="content">
<VtsCardRowKeyValue>
<template #key>{{ t('state') }}</template>
<template #value>
@@ -88,7 +88,7 @@
<VtsCopyButton :value="noMissingPatches ? t('patches-up-to-date') : t('n-missing', nMissingPatches)" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -100,6 +100,7 @@ import { useXoHostMissingPatchesCollection } from '@/modules/host/remote-resourc
import { getHostIcon, getHostState } from '@/modules/host/utils/xo-host.util.ts'
import { useXoPoolCollection } from '@/modules/pool/remote-resources/use-xo-pool-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'
import VtsEnabledState from '@core/components/enabled-state/VtsEnabledState.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
@@ -147,10 +148,6 @@ const hostIcon = computed(() => getHostIcon(host))
<style lang="postcss" scoped>
.host-info-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
.value {
display: flex;
align-items: center;

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="host-network-card">
<UiPanelCard>
<UiPanelCardTitle :label="t('network')" size="medium" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('management-ip') }}</template>
<template #value>{{ host.address }}</template>
@@ -9,37 +9,13 @@
<VtsCopyButton :value="host.address" />
</template>
</VtsCardRowKeyValue>
<template v-if="ipV4Addresses.length > 0">
<VtsCardRowKeyValue v-for="(ip, index) in ipV4Addresses" :key="ip">
<template #key>
<div v-if="index === 0">{{ t('ipv4-addresses') }}</div>
</template>
<template #value>{{ ip }}</template>
<template #addons>
<VtsCopyButton :value="ip" />
<VtsCopyAllMenuItem v-if="index === 0 && ipV4Addresses.length > 1" :values="ipV4Addresses" />
</template>
</VtsCardRowKeyValue>
</template>
<VtsCardRowKeyValue v-else>
<VtsCardRowKeyValue :values="ipV4Addresses">
<template #key>{{ t('ipv4-addresses') }}</template>
</VtsCardRowKeyValue>
<template v-if="ipV6Addresses.length > 0">
<VtsCardRowKeyValue v-for="(ip, index) in ipV6Addresses" :key="ip">
<template #key>
<div v-if="index === 0">{{ t('ipv6-addresses') }}</div>
</template>
<template #value>{{ ip }}</template>
<template #addons>
<VtsCopyButton :value="ip" />
<VtsCopyAllMenuItem v-if="index === 0 && ipV6Addresses.length > 1" :values="ipV6Addresses" />
</template>
</VtsCardRowKeyValue>
</template>
<VtsCardRowKeyValue v-else>
<VtsCardRowKeyValue :values="ipV6Addresses">
<template #key>{{ t('ipv6-addresses') }}</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -47,7 +23,7 @@
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPifCollection } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -74,13 +50,3 @@ const ipV6Addresses = computed(
}, []) ?? []
)
</script>
<style lang="postcss" scoped>
.host-network-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="host-software-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('software')" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('version') }}</template>
<template #value>{{ host.version }}</template>
@@ -9,13 +9,14 @@
<VtsCopyButton :value="host.version" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
<script lang="ts" setup>
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-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'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -27,13 +28,3 @@ defineProps<{
const { t } = useI18n()
</script>
<style scoped lang="postcss">
.host-software-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -12,7 +12,7 @@
icon="object:network"
:href="networkHref"
/>
<div class="content">
<VtsCardRowList class="content">
<!-- DESCRIPTION -->
<VtsCardRowKeyValue truncate align-top>
<template #key>{{ t('description') }}</template>
@@ -56,7 +56,7 @@
<template #key>{{ t('locking-mode-default') }}</template>
<template #value>{{ networkDefaultLockingMode }}</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
<NetworkPifsInfoCard :network />
</template>
@@ -70,6 +70,7 @@ import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-n
import { useXoPifCollection } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.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'
import VtsDeleteButton from '@core/components/delete-button/VtsDeleteButton.vue'
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
@@ -116,10 +117,6 @@ const networkDefaultLockingMode = computed(() => (network?.defaultIsLocked ? t('
<style scoped lang="postcss">
.network-side-panel {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
.value:empty::before {
content: '-';
}

View File

@@ -7,14 +7,14 @@
<!-- PIF -->
<UiPanelCard>
<UiPanelCardTitle :id="pif.id" size="medium" :label="pif.isBondMaster ? t('bond') : t('pif')" />
<div class="content">
<VtsCardRowList>
<!-- NETWORK -->
<VtsCardRowKeyValue>
<template #key>
{{ t('network') }}
</template>
<template #value>
<UiLink v-if="network" size="medium" :to="networkTo" icon="object:network">
<UiLink v-if="network" size="small" :to="networkTo" icon="object:network">
{{ network.name_label }}
</UiLink>
</template>
@@ -77,34 +77,15 @@
<VtsCopyButton :value="network.tags.join(', ')" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
<!-- NETWORK INFORMATION -->
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('network-information')" />
<div class="content">
<VtsCardRowList>
<!-- IP ADDRESSES -->
<template v-if="ipAddresses.length">
<VtsCardRowKeyValue v-for="(ip, index) in ipAddresses" :key="ip">
<template #key>
<div v-if="index === 0">{{ t('ip-addresses') }}</div>
</template>
<template #value>
<span class="text-ellipsis">{{ ip }}</span>
</template>
<template #addons>
<VtsCopyButton :value="ip" />
<VtsCopyAllMenuItem v-if="index === 0 && ipAddresses.length > 1" :values="ipAddresses" />
</template>
</VtsCardRowKeyValue>
</template>
<VtsCardRowKeyValue v-else>
<template #key>
{{ t('ip-addresses') }}
</template>
<template #value>
<span class="value" />
</template>
<VtsCardRowKeyValue :values="ipAddresses">
<template #key>{{ t('ip-addresses') }}</template>
</VtsCardRowKeyValue>
<!-- MAC ADDRESSES -->
<VtsCardRowKeyValue>
@@ -168,24 +149,15 @@
</template>
</VtsCardRowKeyValue>
<!-- BOND DEVICES -->
<div>
<VtsCardRowKeyValue v-for="(device, index) in bondDevices" :key="device">
<template #key>
<div v-if="index === 0">{{ t('bond-devices') }}</div>
</template>
<template #value>{{ device }}</template>
<template #addons>
<VtsCopyButton :value="device" />
<VtsCopyAllMenuItem v-if="index === 0 && bondDevices.length > 1" :values="bondDevices" />
</template>
</VtsCardRowKeyValue>
</div>
</div>
<VtsCardRowKeyValue v-if="bondDevices.length > 0" :values="bondDevices">
<template #key>{{ t('bond-devices') }}</template>
</VtsCardRowKeyValue>
</VtsCardRowList>
</UiPanelCard>
<!-- PROPERTIES -->
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('properties')" />
<div class="content">
<VtsCardRowList>
<!-- MTU -->
<VtsCardRowKeyValue>
<template #key>
@@ -219,7 +191,7 @@
<VtsCopyButton :value="networkNbd" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
</VtsSidePanel>
@@ -231,7 +203,7 @@ import { getPoolNetworkRoute } from '@/modules/network/utils/xo-network.util.ts'
import PifSetManagementButton from '@/modules/pif/components/actions/set-management/PifSetManagementButton.vue'
import { type FrontXoPif, useXoPifCollection } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
@@ -317,12 +289,6 @@ const speed = computed(() => {
<style scoped lang="postcss">
.pif-side-panel {
.content {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.value:empty::before {
content: '-';
}

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="plugin-infos-card">
<UiPanelCard>
<UiPanelCardTitle :id="plugin.id" size="medium" :label="plugin.name" :to="pluginRoute" icon="object:plugin" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('version') }}</template>
<template #value>{{ plugin.version }}</template>
@@ -36,7 +36,7 @@
<template #key>{{ t('load-at-startup') }}</template>
<template #value><VtsStatus :status="plugin.autoload" /></template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
<UiAlert v-if="!plugin.configured" accent="warning">{{ t('plugin:needs-configuration-short') }}</UiAlert>
</UiPanelCard>
</template>
@@ -46,6 +46,7 @@ import { useXoPluginUtils } from '@/modules/plugin/composables/xo-plugin-utils.c
import type { FrontXoPlugin } from '@/modules/plugin/remote-resources/use-xo-plugin-collection.ts'
import { getPluginRoute } from '@/modules/plugin/utils/xo-plugin.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'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import VtsTag from '@core/components/tag/VtsTag.vue'
@@ -66,13 +67,3 @@ const { description, isLoaded, keywords, configurationStatus } = useXoPluginUtil
const pluginRoute = computed(() => getPluginRoute(plugin.id))
</script>
<style scoped lang="postcss">
.plugin-infos-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<VtsSidePanel :has-selection="!!server" class="pool-side-panel" @close="emit('close')">
<VtsSidePanel :has-selection="!!server" @close="emit('close')">
<template v-if="server" #actions>
<PoolConnectionToggleButton :server-id="server.id" />
<MenuList placement="bottom-end">
@@ -34,7 +34,7 @@
:icon="poolInfo.icon"
:disabled="!isConnected"
/>
<div class="content">
<VtsCardRowList>
<!-- Description -->
<VtsCardRowKeyValue truncate align-top>
<template #key>{{ t('description') }}</template>
@@ -55,68 +55,70 @@
<VtsCopyButton :value="pool.tags.join(', ')" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('connection')" />
<!-- status -->
<VtsCardRowKeyValue>
<template #key>{{ t('status') }}</template>
<template #value>
<UiInfo :accent="connectionStatus.accent">
{{ connectionStatus.text }}
</UiInfo>
</template>
</VtsCardRowKeyValue>
<!-- primary-host -->
<VtsCardRowKeyValue>
<template #key>{{ t('master') }}</template>
<template #value>
<HostLink v-if="primaryHost !== undefined" :host="primaryHost" size="small" />
</template>
<template v-if="primaryHost !== undefined" #addons>
<VtsCopyButton :value="primaryHost.id" />
</template>
</VtsCardRowKeyValue>
<!-- ip-address -->
<VtsCardRowKeyValue>
<template #key>{{ t('ip-address') }}</template>
<template #value>{{ server.host }}</template>
<template #addons>
<VtsCopyButton :value="server.host" />
</template>
</VtsCardRowKeyValue>
<!-- proxy-url -->
<VtsCardRowKeyValue>
<template #key>{{ t('proxy-url') }}</template>
<template #value>{{ server.httpProxy }}</template>
<template v-if="server.httpProxy !== undefined" #addons>
<VtsCopyButton :value="server.httpProxy" />
</template>
</VtsCardRowKeyValue>
<!-- username -->
<VtsCardRowKeyValue>
<template #key>{{ t('username') }}</template>
<template #value>{{ server.username }}</template>
<template #addons>
<VtsCopyButton :value="server.username" />
</template>
</VtsCardRowKeyValue>
<!-- read-only -->
<VtsCardRowKeyValue>
<template #key>{{ t('read-only') }}</template>
<template #value>
<VtsStatus :status="server.readOnly" />
</template>
</VtsCardRowKeyValue>
<!-- self-signed-certificates -->
<VtsCardRowKeyValue>
<template #key>{{ t('self-signed-certificates') }}</template>
<template #value>
<!-- todo add information button. waiting modal -->
<VtsStatus :status="server.allowUnauthorized" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowList>
<!-- status -->
<VtsCardRowKeyValue>
<template #key>{{ t('status') }}</template>
<template #value>
<UiInfo :accent="connectionStatus.accent">
{{ connectionStatus.text }}
</UiInfo>
</template>
</VtsCardRowKeyValue>
<!-- primary-host -->
<VtsCardRowKeyValue>
<template #key>{{ t('master') }}</template>
<template #value>
<HostLink v-if="primaryHost !== undefined" :host="primaryHost" size="small" />
</template>
<template v-if="primaryHost !== undefined" #addons>
<VtsCopyButton :value="primaryHost.id" />
</template>
</VtsCardRowKeyValue>
<!-- ip-address -->
<VtsCardRowKeyValue>
<template #key>{{ t('ip-address') }}</template>
<template #value>{{ server.host }}</template>
<template #addons>
<VtsCopyButton :value="server.host" />
</template>
</VtsCardRowKeyValue>
<!-- proxy-url -->
<VtsCardRowKeyValue>
<template #key>{{ t('proxy-url') }}</template>
<template #value>{{ server.httpProxy }}</template>
<template v-if="server.httpProxy !== undefined" #addons>
<VtsCopyButton :value="server.httpProxy" />
</template>
</VtsCardRowKeyValue>
<!-- username -->
<VtsCardRowKeyValue>
<template #key>{{ t('username') }}</template>
<template #value>{{ server.username }}</template>
<template #addons>
<VtsCopyButton :value="server.username" />
</template>
</VtsCardRowKeyValue>
<!-- read-only -->
<VtsCardRowKeyValue>
<template #key>{{ t('read-only') }}</template>
<template #value>
<VtsStatus :status="server.readOnly" />
</template>
</VtsCardRowKeyValue>
<!-- self-signed-certificates -->
<VtsCardRowKeyValue>
<template #key>{{ t('self-signed-certificates') }}</template>
<template #value>
<!-- todo add information button. waiting modal -->
<VtsStatus :status="server.allowUnauthorized" />
</template>
</VtsCardRowKeyValue>
</VtsCardRowList>
</UiPanelCard>
<UiPanelCard v-if="hosts !== undefined">
<UiPanelCardTitle size="medium" :label="t('hosts')" :counter="hosts.length" />
@@ -146,6 +148,7 @@ import { useXoPoolCollection } from '@/modules/pool/remote-resources/use-xo-pool
import { getPoolInfo } from '@/modules/pool/utils/xo-pool.util.ts'
import type { FrontXoServer } from '@/modules/server/remote-resources/use-xo-server-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'
import MenuList from '@core/components/menu/MenuList.vue'
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
@@ -198,13 +201,3 @@ const connectionStatus = useMapper(
'error'
)
</script>
<style scoped lang="postcss">
.pool-side-panel {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<UiLink :to="vdiRoute" :icon="vbdsSnapshotStatus" size="medium">
<UiLink :to="vdiRoute" :icon="vbdsSnapshotStatus" size="small">
{{ vdi.name_label }}
</UiLink>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<UiPanelCard class="snapshot-info-card">
<UiPanelCard>
<UiPanelCardTitle
:id="snapshot.id"
size="medium"
@@ -7,7 +7,7 @@
:href="buildXo5VmSnapshotRoute(snapshot.$snapshot_of, snapshot.id)"
icon="object:vm-snapshot"
/>
<div class="content">
<VtsCardRowList>
<!-- DESCRIPTION -->
<VtsCardRowKeyValue truncate align-top>
<template #key>
@@ -53,12 +53,12 @@
{{ t('memory') }}
</template>
<template #value>
<UiInfo :accent="memoryAccent" size="medium">
<UiInfo :accent="memoryAccent">
{{ t(memoryLabel) }}
</UiInfo>
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -67,6 +67,7 @@ import type { FrontXoVmSnapshot } from '@/modules/snapshot/components/remote-res
import { useSnapshotTrigger } from '@/modules/snapshot/composables/xo-snapshot-trigger.composable.ts'
import { useXo5VmSnapshotRoute } from '@/modules/snapshot/composables/xo-vm-snapshot-route-xo5.composable.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'
import UiInfo from '@core/components/ui/info/UiInfo.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
@@ -93,13 +94,3 @@ const memoryLabel = computed(() => (snapshot.power_state === VM_POWER_STATE.SUSP
const trigger = computed(() => getSnapshotTrigger(snapshot))
</script>
<style scoped lang="postcss">
.snapshot-info-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<UiPanelCard v-if="vmSnapshotVdiSnapshots.length > 0" class="snapshot-vdi-card">
<UiPanelCardTitle size="medium" :label="t('vdis')" :counter="vmSnapshotVdiSnapshots.length" />
<div class="content">
<div v-for="(vmSnapshotVdi, index) in vmSnapshotVdiSnapshots" :key="vmSnapshotVdi.id" class="content">
<VtsCardRowList class="content">
<VtsCardRowList v-for="(vmSnapshotVdi, index) in vmSnapshotVdiSnapshots" :key="vmSnapshotVdi.id" class="content">
<SnapshotVdiLinkCardItem :vdi="vmSnapshotVdi" />
<!-- DESCRIPTION -->
<VtsCardRowKeyValue truncate>
@@ -19,8 +19,8 @@
<!-- FORMAT -->
<VdiFormatCardItem :format="vmSnapshotVdi.image_format" />
<VtsDivider v-if="index < vmSnapshotVdiSnapshots.length - 1" class="divider" type="stretch" />
</div>
</div>
</VtsCardRowList>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -31,6 +31,7 @@ import type { FrontXoVmSnapshot } from '@/modules/snapshot/components/remote-res
import { useXoVmSnapshotVdiCollection } from '@/modules/snapshot/components/remote-resources/use-xo-vm-snapshot-vdi-collection.ts'
import VdiFormatCardItem from '@/modules/vdi/components/list/panel/card-items/VdiFormatCardItem.vue'
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'
import VtsDivider from '@core/components/divider/VtsDivider.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
@@ -47,10 +48,6 @@ const { vmSnapshotVdiSnapshots } = useXoVmSnapshotVdiCollection({}, () => snapsh
<style scoped lang="postcss">
.snapshot-vdi-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
.divider {
margin-block: 1.6rem;
}

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="card-container">
<UiPanelCard>
<UiPanelCardTitle :id="sr.id" size="medium" :label="sr.name_label" :to="srRoute" :icon="srStatusIcon" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('status') }}</template>
<template #value>
@@ -51,7 +51,7 @@
<template #key>{{ t('high-availability') }}</template>
<template #value><VtsStatus :status="isHaSr" /></template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -64,6 +64,7 @@ import {
import { getSrPageLocation } from '@/modules/storage-repository/utils/xo-sr.util.ts'
import type { SrScope } from '@core/types/storage-repository.type.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'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import VtsTag from '@core/components/tag/VtsTag.vue'
@@ -95,13 +96,3 @@ const allocationStrategy = computed(() => getSrProvisioningLabel(sr))
const isHaSr = isHighAvailabilitySr(() => sr)
</script>
<style scoped lang="postcss">
.storage-repository-infos-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -5,10 +5,10 @@
{{ t('no-pbd-attached') }}
</VtsStateHero>
<template v-else>
<div class="content">
<VtsCardRowList class="content">
<VtsStatus :status="srConnectionStatus" />
</div>
<div v-if="areSomePbdsDisconnected" class="content">
</VtsCardRowList>
<VtsCardRowList v-if="areSomePbdsDisconnected" class="content">
<template v-for="(pbd, index) in disconnectedPbds" :key="pbd.id">
<VtsDivider v-if="index > 0" class="divider" type="stretch" />
<UiPanelCardTitle size="small" :label="t('disconnected-pbd-number', { n: index + 1 })" class="subtitle" />
@@ -29,7 +29,7 @@
accent="info"
/>
</template>
</div>
</VtsCardRowList>
</template>
</UiPanelCard>
</template>
@@ -41,6 +41,7 @@ import { useXoSrUtils } from '@/modules/storage-repository/composables/xo-sr-uti
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import type { SrScope } from '@core/types/storage-repository.type.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsDivider from '@core/components/divider/VtsDivider.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
@@ -68,10 +69,6 @@ const { areSomePbdsDisconnected, disconnectedPbds } = useXoPbdUtils(pbdsInScope)
<style scoped lang="postcss">
.storage-repository-pbds-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
.subtitle {
margin-block-end: 0.4rem;
}

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="task-errors-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('errors')" :counter="{ value: 1, accent: 'danger' }" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('message') }}</template>
<template #value>{{ task.result?.message }}</template>
@@ -9,7 +9,7 @@
<VtsCopyButton :value="String(task.result.message)" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
<UiLogEntryViewer
v-if="task.result?.stack"
:content="task.result.stack"
@@ -23,6 +23,7 @@
<script lang="ts" setup>
import type { FrontXoTask } from '@/modules/task/remote-resources/use-xo-task-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'
import UiLogEntryViewer from '@core/components/ui/log-entry-viewer/UiLogEntryViewer.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
@@ -35,13 +36,3 @@ defineProps<{
const { t } = useI18n()
</script>
<style scoped lang="postcss">
.task-errors-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="task-infos-card">
<UiPanelCardTitle size="medium" :label="t('info')" :counter="{ value: task.infos?.length ?? 0, accent: 'info' }" />
<div class="content">
<VtsCardRowList class="content">
<template v-for="(info, index) in task.infos" :key="index">
<VtsDivider v-if="index > 0" class="divider" type="stretch" />
<VtsCardRowKeyValue>
@@ -12,13 +12,14 @@
</template>
</VtsCardRowKeyValue>
</template>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
<script lang="ts" setup>
import type { FrontXoTask } from '@/modules/task/remote-resources/use-xo-task-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'
import VtsDivider from '@core/components/divider/VtsDivider.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
@@ -35,10 +36,6 @@ const { t } = useI18n()
<style scoped lang="postcss">
.task-infos-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
.divider {
margin-block: 1.6rem;
}

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="task-objects-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('object')" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('id') }}</template>
<template #value>{{ task.properties.objectId }}</template>
@@ -9,13 +9,14 @@
<VtsCopyButton :value="String(task.properties.objectId)" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
<script lang="ts" setup>
import type { FrontXoTask } from '@/modules/task/remote-resources/use-xo-task-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'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -27,13 +28,3 @@ defineProps<{
const { t } = useI18n()
</script>
<style scoped lang="postcss">
.task-objects-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -9,13 +9,13 @@
<template v-if="nameParts" #label>
<span>
<template v-for="(part, index) in nameParts" :key="index">
<UiLink v-if="part.to !== undefined" size="medium" :to="part.to">{{ part.text }}</UiLink>
<UiLink v-if="part.to !== undefined" size="small" :to="part.to">{{ part.text }}</UiLink>
<template v-else>{{ part.text }}</template>
</template>
</span>
</template>
</UiPanelCardTitle>
<div class="content">
<VtsCardRowList class="content">
<VtsCardRowKeyValue>
<template #key>{{ t('task-type') }}</template>
<template #value>
@@ -66,7 +66,7 @@
<UiCircleProgressBar :accent="progressAccent" size="small" :value="progress" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -76,6 +76,7 @@ import type { FrontXoTask } from '@/modules/task/remote-resources/use-xo-task-co
import { getTaskAccents } from '@/modules/task/utils/xo-task.util.ts'
import { useXoUserResource } from '@/modules/user/remote-resources/use-xo-user.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'
import VtsTag from '@core/components/tag/VtsTag.vue'
import UiCircleProgressBar from '@core/components/ui/circle-progress-bar/UiCircleProgressBar.vue'
@@ -146,10 +147,6 @@ const formattedEndDate = computed(() => {
<style scoped lang="postcss">
.task-quick-infos-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
.user {
display: flex;
align-items: center;

View File

@@ -5,7 +5,7 @@
:label="t('warnings')"
:counter="{ value: task.warnings?.length ?? 0, accent: 'warning' }"
/>
<div class="content">
<VtsCardRowList class="content">
<template v-for="(warning, index) in task.warnings" :key="index">
<VtsDivider v-if="index > 0" class="divider" type="stretch" />
<VtsCardRowKeyValue>
@@ -16,13 +16,14 @@
</template>
</VtsCardRowKeyValue>
</template>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
<script lang="ts" setup>
import type { FrontXoTask } from '@/modules/task/remote-resources/use-xo-task-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'
import VtsDivider from '@core/components/divider/VtsDivider.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
@@ -39,10 +40,6 @@ const { t } = useI18n()
<style scoped lang="postcss">
.task-warnings-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
.divider {
margin-block: 1.6rem;
}

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="traffic-rule-network-infos-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('network')" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>
{{ t('network') }}
@@ -82,7 +82,7 @@
<VtsCopyButton :value="networkDefaultLockingMode" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -91,6 +91,7 @@ import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-n
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'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
@@ -124,13 +125,3 @@ const networkNbd = computed(() => (network.nbd ? t('on') : t('off')))
const networkDefaultLockingMode = computed(() => (network.defaultIsLocked ? t('disabled') : t('unlocked')))
</script>
<style scoped lang="postcss">
.traffic-rule-network-infos-card {
.content {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="traffic-rule-summary-card">
<UiPanelCardTitle size="medium" :label="t('traffic-rule')" icon="fa:traffic-rule" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>
{{ t('rule-type') }}
@@ -44,7 +44,7 @@
<VtsCopyButton :value="description" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -57,6 +57,7 @@ import { useXoVifCollection } from '@/modules/vif/remote-resources/use-xo-vif-co
import { useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import { RULE_STATUS } from '@/shared/constants.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'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
@@ -127,12 +128,6 @@ const vifDevice = computed(() => `${t('vif')}${vif.value?.device}`)
<style scoped lang="postcss">
.traffic-rule-summary-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.value-content {
display: inline-flex;
flex-wrap: wrap;

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="traffic-rule-vif-infos-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('vif')" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>
{{ t('name') }}
@@ -115,7 +115,7 @@
{{ vif.txChecksumming }}
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -127,6 +127,7 @@ import { useXoVifUtils } from '@/modules/vif/composables/xo-vif-utils.composable
import type { FrontXoVif } from '@/modules/vif/remote-resources/use-xo-vif-collection.ts'
import { useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsCodeSnippet from '@core/components/code-snippet/VtsCodeSnippet.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
@@ -176,13 +177,3 @@ const networkVlan = computed(() => {
const vifDevice = computed(() => `${t('vif')}${vif.device}`)
</script>
<style scoped lang="postcss">
.traffic-rule-vif-infos-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,26 +1,9 @@
<template>
<UiPanelCard class="traffic-rule-vif-network-info-card">
<UiPanelCardTitle size="medium" :label="t('vif-network-info')" />
<div class="content">
<template v-if="ipAddresses.length">
<VtsCardRowKeyValue v-for="(ip, index) in ipAddresses" :key="ip" align-top>
<template #key>
<div v-if="index === 0">{{ t('ip-addresses') }}</div>
</template>
<template #value>{{ ip }}</template>
<template #addons>
<VtsCopyButton :value="ip" />
<VtsCopyAllMenuItem v-if="index === 0 && ipAddresses.length > 1" :values="ipAddresses" />
</template>
</VtsCardRowKeyValue>
</template>
<VtsCardRowKeyValue v-else>
<template #key>
{{ t('ip-addresses') }}
</template>
<template #value>
<span class="value" />
</template>
<VtsCardRowList class="content">
<VtsCardRowKeyValue :values="ipAddresses" align-top>
<template #key>{{ t('ip-addresses') }}</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue>
<template #key>
@@ -33,7 +16,7 @@
<VtsCopyButton :value="vif.MAC" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -41,7 +24,7 @@
import type { FrontXoVif } from '@/modules/vif/remote-resources/use-xo-vif-collection.ts'
import { useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -65,10 +48,6 @@ const ipAddresses = computed(() => {
<style scoped lang="postcss">
.traffic-rule-vif-network-info-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
.value:empty::before {
content: '-';
}

View File

@@ -4,13 +4,13 @@
<template #label>
<span class="name">
<UiUserLogo size="small" />
<UiLink :href="xo5UsersHref" size="medium">
<UiLink :href="xo5UsersHref" size="small">
{{ user.name }}
</UiLink>
</span>
</template>
</UiPanelCardTitle>
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue truncate align-top>
<template #key>{{ t('xoa-manager') }}</template>
<template #value>{{ userPermission }}</template>
@@ -36,7 +36,7 @@
<template #key>{{ t('providers') }}</template>
<template #value>{{ providers }}</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -45,6 +45,7 @@ import { useXoUserAuthenticationTokensCollection } from '@/modules/user/remote-r
import type { FrontXoUser } from '@/modules/user/remote-resources/use-xo-user-collection.ts'
import { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.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'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
@@ -104,11 +105,5 @@ const userPermission = computed(() => {
align-items: center;
gap: 0.8rem;
}
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="vdi-configuration-card">
<UiPanelCardTitle size="medium" :label="t('configuration')" />
<div class="content">
<VtsCardRowList>
<VdiFormatCardItem :format="vdi.image_format" />
<VtsCardRowKeyValue>
<template #key>
@@ -42,7 +42,7 @@
<VtsStatus :status="isBootable" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -54,6 +54,7 @@ import type { FrontXoVdi } from '@/modules/vdi/remote-resources/use-xo-vdi-colle
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.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'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
@@ -89,12 +90,6 @@ const isBootable = computed(() => vbd.value?.bootable ?? false)
<style scoped lang="postcss">
.vdi-configuration-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.storage {
display: flex;
gap: 0.8rem;

View File

@@ -1,5 +1,5 @@
<template>
<UiPanelCard class="vdi-infos-card">
<UiPanelCard>
<UiPanelCardTitle
:id="vdi.id"
size="medium"
@@ -7,7 +7,7 @@
:to="{ name: '/vdi/[id]/general', params: { id: vdi.id }, query: { from: VDI_PAGE_CONTEXT.VM } }"
:icon="vdiIcon"
/>
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue truncate align-top>
<template #key>{{ t('description') }}</template>
<template #value>{{ vdi.name_description }}</template>
@@ -42,7 +42,7 @@
<VtsCopyButton :value="vdiDevice" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -54,6 +54,7 @@ import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collecti
import { VDI_PAGE_CONTEXT } from '@/shared/constants.ts'
import type { IconName } from '@core/icons'
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'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import VtsTag from '@core/components/tag/VtsTag.vue'
@@ -98,13 +99,3 @@ const { notCdDriveVbds } = useXoVmVbdsUtils(() => vm)
const vdiDevice = computed(() => notCdDriveVbds.value.find(vbd => vbd.VDI === vdi.id)?.device ?? '')
</script>
<style scoped lang="postcss">
.vdi-infos-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -10,14 +10,14 @@
<!-- VIF -->
<UiPanelCard>
<UiPanelCardTitle :id="vif.id" size="medium" :to="vifTo" :label="t('vif')" icon="object:vif" />
<div class="content">
<VtsCardRowList>
<!-- NETWORK -->
<VtsCardRowKeyValue>
<template #key>
{{ t('network') }}
</template>
<template #value>
<UiLink v-if="network" size="medium" :to="networkTo" icon="object:network">
<UiLink v-if="network" size="small" :to="networkTo" icon="object:network">
{{ network.name_label }}
</UiLink>
</template>
@@ -85,32 +85,15 @@
{{ vif.txChecksumming }}
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
<!-- NETWORK INFORMATION -->
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('network-information')" />
<div class="content">
<VtsCardRowList>
<!-- IP ADDRESSES -->
<template v-if="ipAddresses.length">
<VtsCardRowKeyValue v-for="(ip, index) in ipAddresses" :key="ip">
<template #key>
<div v-if="index === 0">{{ t('ip-addresses') }}</div>
</template>
<template #value>{{ ip }}</template>
<template #addons>
<VtsCopyButton :value="ip" />
<VtsCopyAllMenuItem v-if="index === 0 && ipAddresses.length > 1" :values="ipAddresses" />
</template>
</VtsCardRowKeyValue>
</template>
<VtsCardRowKeyValue v-else>
<template #key>
{{ t('ip-addresses') }}
</template>
<template #value>
<span class="value" />
</template>
<VtsCardRowKeyValue :values="ipAddresses">
<template #key>{{ t('ip-addresses') }}</template>
</VtsCardRowKeyValue>
<!-- MAC ADDRESSES -->
<VtsCardRowKeyValue>
@@ -124,7 +107,7 @@
<VtsCopyButton :value="vif.MAC" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
</VtsSidePanel>
@@ -139,7 +122,7 @@ import type { FrontXoVif } from '@/modules/vif/remote-resources/use-xo-vif-colle
import { getVifTrafficRoute } from '@/modules/vif/utils/xo-vif.util.ts'
import { type FrontXoVm, useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
@@ -188,12 +171,6 @@ const status = computed(() => (vif?.attached ? CONNECTION_STATUS.CONNECTED : CON
<style scoped lang="postcss">
.vif-side-panel {
.content {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.value:empty::before {
content: '-';
}

View File

@@ -7,7 +7,7 @@
:to="{ name: '/vm/[id]/dashboard', params: { id: vm.id } }"
:icon="`object:vm:${toLower(vm.power_state)}`"
/>
<div class="content">
<VtsCardRowList class="content">
<VtsCardRowKeyValue>
<template #key>{{ t('state') }}</template>
<template #value>
@@ -116,7 +116,7 @@
<VtsCopyButton :value="relativeStartTime" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -130,6 +130,7 @@ import { type FrontXoVm, useXoVmCollection } from '@/modules/vm/remote-resources
import { useXoVmTemplateCollection } from '@/modules/vm/remote-resources/use-xo-vm-template-collection.ts'
import { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.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'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsTag from '@core/components/tag/VtsTag.vue'
@@ -179,10 +180,6 @@ const { powerState, installDateFormatted, relativeStartTime, guestToolsDisplay }
<style scoped lang="postcss">
.vm-info-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
.value {
display: flex;
align-items: center;

View File

@@ -1,8 +1,10 @@
import VmNetworkCard from '@/modules/vm/components/list/panel/cards/VmNetworkCard.vue'
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import { createVm } from '@/test/create-vm.ts'
import { findCardCopiedValues, findCardLabelledValueLists } from '@/test/find-rendered-values.ts'
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
import { t } from '@/test/i18n.ts'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
import { mount } from '@vue/test-utils'
function mountNetworkCard(vm: FrontXoVm = createVm()) {
@@ -12,33 +14,37 @@ function mountNetworkCard(vm: FrontXoVm = createVm()) {
})
}
function findIpAddresses(wrapper: ReturnType<typeof mountNetworkCard>) {
return wrapper.findAll('.vts-card-row-key-value').map(row => row.get('.value').text())
}
it('renders the card title', () => {
const wrapper = mountNetworkCard(createVm({ addresses: undefined }))
expect(wrapper.get('.ui-panel-card-title .title').text()).toBe(t('networks'))
})
it('lists the IP addresses of the VM sorted alphabetically', () => {
it('lists the IP addresses of the VM sorted alphabetically, under a single label', () => {
const wrapper = mountNetworkCard(createVm({ addresses: { '0/ipv4/0': '10.0.0.2', '0/ipv4/1': '10.0.0.1' } }))
expect(findIpAddresses(wrapper)).toEqual(['10.0.0.1', '10.0.0.2'])
expect(findCardLabelledValueLists(wrapper)).toEqual({ [t('ip-addresses')]: ['10.0.0.1', '10.0.0.2'] })
})
it('labels only the first address row', () => {
it('offers to copy each IP address and all of them', () => {
const wrapper = mountNetworkCard(createVm({ addresses: { '0/ipv4/0': '10.0.0.1', '0/ipv4/1': '10.0.0.2' } }))
expect(wrapper.findAll('.vts-card-row-key-value').map(row => row.get('.key').text())).toEqual([t('ip-addresses'), ''])
expect(findCardCopiedValues(wrapper)).toEqual(['10.0.0.1', '10.0.0.2'])
expect(wrapper.getComponent(VtsCopyAllMenuItem).props('values')).toEqual(['10.0.0.1', '10.0.0.2'])
})
it('offers no copy of all the IP addresses for a single one', () => {
const wrapper = mountNetworkCard(createVm({ addresses: { '0/ipv4/0': '10.0.0.1' } }))
expect(findCardCopiedValues(wrapper)).toEqual(['10.0.0.1'])
expect(wrapper.findComponent(VtsCopyAllMenuItem).exists()).toBe(false)
})
it('shows an empty address row when the VM has no IP address', () => {
const wrapper = mountNetworkCard(createVm({ addresses: undefined }))
expect(wrapper.findAll('.vts-card-row-key-value').map(row => row.get('.key').text())).toEqual([t('ip-addresses')])
expect(findIpAddresses(wrapper)).toEqual([''])
expect(findCardLabelledValueLists(wrapper)).toEqual({ [t('ip-addresses')]: [''] })
expect(findCardCopiedValues(wrapper)).toEqual([])
})
it('links to the networks page when the VM has at least one IP address', () => {

View File

@@ -1,29 +1,17 @@
<template>
<UiPanelCard class="vm-network-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('networks')">
<template v-if="ipAddresses.length > 0" #action>
<UiLink size="medium" :to="{ name: '/vm/[id]/networks', params: { id: vm.id } }">
<UiLink size="small" :to="{ name: '/vm/[id]/networks', params: { id: vm.id } }">
{{ t('see-details') }}
</UiLink>
</template>
</UiPanelCardTitle>
<div class="content">
<template v-if="ipAddresses.length > 0">
<VtsCardRowKeyValue v-for="(ip, index) in ipAddresses" :key="ip">
<template #key>
<div v-if="index === 0">{{ t('ip-addresses') }}</div>
</template>
<template #value>{{ ip }}</template>
<template #addons>
<VtsCopyButton :value="ip" />
<VtsCopyAllMenuItem v-if="index === 0 && ipAddresses.length > 1" :values="ipAddresses" />
</template>
</VtsCardRowKeyValue>
</template>
<VtsCardRowKeyValue v-else>
<VtsCardRowList>
<VtsCardRowKeyValue :values="ipAddresses">
<template #key>{{ t('ip-addresses') }}</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -31,8 +19,7 @@
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
import { getVmIpAddresses } from '@/modules/vm/utils/xo-vm.util.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import VtsCardRowList from '@core/components/card/VtsCardRowList.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -47,13 +34,3 @@ const { t } = useI18n()
const ipAddresses = computed(() => getVmIpAddresses(vm))
</script>
<style scoped lang="postcss">
.vm-network-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<UiPanelCard class="vm-resources-card">
<UiPanelCard>
<UiPanelCardTitle size="medium" :label="t('resources')" />
<div class="content">
<VtsCardRowList>
<VtsCardRowKeyValue>
<template #key>{{ t('vcpus') }}</template>
<template #value>{{ vm.CPUs.number }}</template>
@@ -37,7 +37,7 @@
<VtsCopyButton :value="String(vm.snapshots.length)" />
</template>
</VtsCardRowKeyValue>
</div>
</VtsCardRowList>
</UiPanelCard>
</template>
@@ -46,6 +46,7 @@ import { useXoVbdCollection } from '@/modules/vbd/remote-resources/use-xo-vbd-co
import { useXoVdiCollection } from '@/modules/vdi/remote-resources/use-xo-vdi-collection.ts'
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-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'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
@@ -75,13 +76,3 @@ const formattedDiskSpace = computed(() => {
return formatSize(totalSize, 1)
})
</script>
<style scoped lang="postcss">
.vm-resources-card {
.content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
</style>

View File

@@ -37,6 +37,22 @@ export function findCardLabelledValues(wrapper: QueryableWrapper): Record<string
return Object.fromEntries(findPairs(wrapper, { root: CARD_KEY_VALUE_ROW_SELECTOR, label: '.key', value: '.value' }))
}
/**
* Reads every `VtsCardRowKeyValue` as `{ label: values }`, each value of a
* multi-value row (an address list, report recipients) being its own entry, in
* the order the row lists them. A single-value row reads as a one-entry list.
*/
export function findCardLabelledValueLists(wrapper: QueryableWrapper): Record<string, string[]> {
return Object.fromEntries(
wrapper
.findAll(CARD_KEY_VALUE_ROW_SELECTOR)
.map(row => [
row.get('.key').text(),
row.findAll('.value-row > .value, .value-container > .value').map(value => value.text()),
])
)
}
/**
* Reads the value each `VtsTabularKeyValueRow` / `VtsKeyValueRow` offers to copy, keyed
* by its label. Rows without a copy button are left out, so a single `toEqual` pins

View File

@@ -12,6 +12,7 @@
> Users must be able to say: "Nice enhancement, I'm eager to test it"
- [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))
### Bug fixes