mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
Compare commits
14 Commits
feat/vates
...
abdcbc1088
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abdcbc1088 | ||
|
|
3c4ae9c70f | ||
|
|
8e81250ca3 | ||
|
|
c8c11ae63c | ||
|
|
8df236a512 | ||
|
|
8cc3384850 | ||
|
|
e4cc8df323 | ||
|
|
43a7e3d3ac | ||
|
|
2e586f0369 | ||
|
|
93dc079f71 | ||
|
|
9c6a59bb89 | ||
|
|
12aa9576b9 | ||
|
|
85541ce5d7 | ||
|
|
56a33c1639 |
@@ -772,6 +772,7 @@ export type RawTrafficRule = {
|
||||
ipRange: string
|
||||
direction: TrafficRuleDirection
|
||||
port?: string
|
||||
priority?: number
|
||||
}
|
||||
|
||||
type BaseTrafficRule = RawTrafficRule & {
|
||||
|
||||
@@ -9,7 +9,7 @@ async function consumeGenerator(source: AsyncGenerator) {
|
||||
return 'finished'
|
||||
}
|
||||
test('Disk class', async t => {
|
||||
const testRes = await t.test('synchronised fork disk ', async () => {
|
||||
const testRes = await t.test('synchronized fork disk ', async () => {
|
||||
const blockSize = 1024
|
||||
const nbBlocks = 20
|
||||
const source = new DebugDisk({ nbBlocks, blockSize, fillRate: 100 })
|
||||
|
||||
@@ -18,7 +18,7 @@ ee.on('start', async function () {
|
||||
// listeners have settled
|
||||
await ee.emitAsync('start')
|
||||
|
||||
// by default, it will rejects as soon as one listener reject, you can customise
|
||||
// by default, it will rejects as soon as one listener reject, you can customize
|
||||
// error handling though:
|
||||
await ee.emitAsync(
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@ ee.on('start', async function () {
|
||||
// listeners have settled
|
||||
await ee.emitAsync('start')
|
||||
|
||||
// by default, it will rejects as soon as one listener reject, you can customise
|
||||
// by default, it will rejects as soon as one listener reject, you can customize
|
||||
// error handling though:
|
||||
await ee.emitAsync(
|
||||
{
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# ChangeLog
|
||||
|
||||
## **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))
|
||||
- [Treeview] Fix translations of the tooltip for VM and host statuses in treeview (PR [#10643](https://github.com/vatesfr/xen-orchestra/pull/10643))
|
||||
|
||||
## **0.27.0** (2026-10-29)
|
||||
|
||||
- [New network] Choose between no NBD, secure NBD and insecure NBD (not encrypted) when creating a network (PR [#10540](https://github.com/vatesfr/xen-orchestra/pull/10540))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<VtsQuickInfoCard class="vm-dashboard-quick-info" :loading="!isReady">
|
||||
<VtsQuickInfoCard :loading="!isReady" class="vm-dashboard-quick-info">
|
||||
<VtsKeyValueList>
|
||||
<VtsKeyValueRow :label="t('state')">
|
||||
<template #value>
|
||||
@@ -19,11 +19,11 @@
|
||||
<template #value>
|
||||
<UiLink
|
||||
v-if="host"
|
||||
:to="`/host/${host.uuid}`"
|
||||
size="medium"
|
||||
:icon="hostIcon"
|
||||
:is-primary="isPoolMaster"
|
||||
:primary-tooltip="t('master')"
|
||||
:to="`/host/${host.uuid}`"
|
||||
size="medium"
|
||||
>
|
||||
{{ host.name_label }}
|
||||
</UiLink>
|
||||
@@ -54,9 +54,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums.ts'
|
||||
import type { XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import { getHostIcon } from '@/modules/host/utils/host.util.ts'
|
||||
import { useVmUtils } from '@/modules/vm/composables/vm-utils.composable.ts'
|
||||
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
|
||||
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
|
||||
import { useVmGuestMetricsStore } from '@/stores/xen-api/vm-guest-metrics.store.ts'
|
||||
@@ -70,7 +70,6 @@ import VtsTag from '@core/components/tag/VtsTag.vue'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
|
||||
import { useTimeAgo } from '@core/composables/locale-time-ago.composable.ts'
|
||||
import { useMapper } from '@core/packages/mapper'
|
||||
import { formatSizeRaw } from '@core/utils/size.util.ts'
|
||||
import { formatDate, parseDateTime } from '@core/utils/time.util.ts'
|
||||
import { computed } from 'vue'
|
||||
@@ -98,16 +97,7 @@ const isPoolMaster = computed(() => pool.value?.master === host.value?.$ref)
|
||||
|
||||
const hostIcon = computed(() => getHostIcon(host.value, getHostPowerState(host.value)))
|
||||
|
||||
const powerState = useMapper(
|
||||
() => vm.power_state,
|
||||
{
|
||||
[VM_POWER_STATE.RUNNING]: { icon: 'status:running-circle', text: t('vm:status:running') },
|
||||
[VM_POWER_STATE.HALTED]: { icon: 'status:halted-circle', text: t('vm:status:halted') },
|
||||
[VM_POWER_STATE.PAUSED]: { icon: 'status:paused-circle', text: t('vm:status:paused') },
|
||||
[VM_POWER_STATE.SUSPENDED]: { icon: 'status:suspended-circle', text: t('vm:status:suspended') },
|
||||
},
|
||||
VM_POWER_STATE.RUNNING
|
||||
)
|
||||
const { powerState } = useVmUtils(() => vm)
|
||||
|
||||
function getValidDateTime(dateTime: string | undefined): string | undefined {
|
||||
return dateTime === '19700101T00:00:00Z' ? undefined : dateTime
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
|
||||
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
|
||||
import type { IconName } from '@core/icons'
|
||||
import { useTimeAgo } from '@core/composables/locale-time-ago.composable.ts'
|
||||
import { useMapper } from '@core/packages/mapper'
|
||||
import { createMapper, useMapper } from '@core/packages/mapper'
|
||||
import { parseDateTime } from '@core/utils/time.util.ts'
|
||||
import { toComputed } from '@core/utils/to-computed.util.ts'
|
||||
import { HOST_POWER_STATE } from '@vates/types'
|
||||
@@ -70,20 +70,31 @@ export function useHostUtils(rawHost: MaybeRefOrGetter<XenApiHost | undefined>)
|
||||
|
||||
const currentOperation = useMapper<string, string>(
|
||||
() => pendingStateOperation.value,
|
||||
{
|
||||
() => ({
|
||||
[HOST_OPERATION.ENABLE]: t('operation:enable'),
|
||||
[HOST_OPERATION.EVACUATE]: t('operation:evacuate'),
|
||||
[HOST_OPERATION.POWER_ON]: t('operation:start'),
|
||||
[HOST_OPERATION.REBOOT]: t('operation:clean-reboot'),
|
||||
[HOST_OPERATION.SHUTDOWN]: t('operation:shutdown'),
|
||||
unknown: '',
|
||||
},
|
||||
}),
|
||||
'unknown'
|
||||
)
|
||||
|
||||
const getHostStatus = createMapper<HostState, { text: string; icon: IconName | undefined }>(
|
||||
() => ({
|
||||
running: { text: t('host:status:running'), icon: 'status:running-circle' },
|
||||
halted: { text: t('host:status:halted'), icon: 'status:halted-circle' },
|
||||
disabled: { text: t('host:status:disabled'), icon: 'status:host-disabled-circle' },
|
||||
unknown: { text: t('host:status:unknown'), icon: undefined },
|
||||
}),
|
||||
'unknown'
|
||||
)
|
||||
|
||||
return {
|
||||
isMaster,
|
||||
isRunning,
|
||||
getHostStatus,
|
||||
hostState,
|
||||
powerState,
|
||||
isChangingState,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
>
|
||||
{{ branch.data.name_label || '(Host)' }}
|
||||
<template #icon>
|
||||
<VtsObjectIcon v-tooltip="hostState" type="host" size="medium" :state="hostState" />
|
||||
<VtsObjectIcon v-tooltip="powerState.text" type="host" size="medium" :state="hostState" />
|
||||
</template>
|
||||
<template #addons>
|
||||
<UiLoader v-if="isChangingState" v-tooltip="currentOperation" />
|
||||
@@ -63,7 +63,8 @@ const { t } = useI18n()
|
||||
const { isMasterHost } = usePoolStore().subscribe()
|
||||
const { runningVmsCountByHostRef } = useVmStore().subscribe()
|
||||
|
||||
const { hostState, isChangingState, currentOperation } = useHostUtils(() => branch.data)
|
||||
const { getHostStatus, hostState, isChangingState, currentOperation } = useHostUtils(() => branch.data)
|
||||
const powerState = computed(() => getHostStatus(hostState.value))
|
||||
|
||||
const isMaster = computed(() => isMasterHost(branch.data.$ref))
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<VtsTreeItem expanded :node-id="leaf.dataId">
|
||||
<VtsTreeItem :node-id="leaf.dataId" expanded>
|
||||
<UiTreeItemLabel :route="{ name: '/vm/[uuid]', params: { uuid: leaf.data.uuid } }" no-indent>
|
||||
{{ leaf.data.name_label || '(VM)' }}
|
||||
<template #icon>
|
||||
<VtsObjectIcon size="medium" :state="powerState" type="vm" />
|
||||
<VtsObjectIcon v-tooltip="powerState.text" :state="vmState" size="medium" type="vm" />
|
||||
</template>
|
||||
<template #addons>
|
||||
<UiLoader v-if="isChangingState" v-tooltip="{ placement: 'top', content: currentOperation }" />
|
||||
@@ -11,10 +11,10 @@
|
||||
<template #trigger="{ open, isOpen }">
|
||||
<UiButtonIcon
|
||||
v-tooltip="{ placement: 'top', content: t('quick-actions') }"
|
||||
icon="action:more-actions"
|
||||
accent="brand"
|
||||
size="small"
|
||||
:selected="isOpen"
|
||||
accent="brand"
|
||||
icon="action:more-actions"
|
||||
size="small"
|
||||
@click="open($event)"
|
||||
/>
|
||||
</template>
|
||||
@@ -26,10 +26,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums.ts'
|
||||
import type { VmLeaf } from '@/modules/treeview/types/tree.type.ts'
|
||||
import VmTreeActions from '@/modules/vm/components/actions/VmTreeActions.vue'
|
||||
import { useVmOperation } from '@/modules/vm/composables/vm-operation.composable.ts'
|
||||
import { useVmUtils } from '@/modules/vm/composables/vm-utils.composable.ts'
|
||||
import MenuList from '@core/components/menu/MenuList.vue'
|
||||
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
|
||||
import VtsTreeItem from '@core/components/tree/VtsTreeItem.vue'
|
||||
@@ -37,6 +37,7 @@ import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
||||
import UiLoader from '@core/components/ui/loader/UiLoader.vue'
|
||||
import UiTreeItemLabel from '@core/components/ui/tree-item-label/UiTreeItemLabel.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive.ts'
|
||||
import { toLower } from 'lodash-es'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -47,6 +48,7 @@ const { leaf } = defineProps<{
|
||||
const { t } = useI18n()
|
||||
|
||||
const { isChangingState, currentOperation } = useVmOperation(() => leaf.data)
|
||||
const { powerState } = useVmUtils(() => leaf.data)
|
||||
|
||||
const powerState = computed(() => leaf.data.power_state.toLowerCase() as Lowercase<VM_POWER_STATE>)
|
||||
const vmState = computed(() => toLower(leaf.data.power_state))
|
||||
</script>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '-';
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function useVmOperation(rawVm: MaybeRefOrGetter<XenApiVm | undefined>) {
|
||||
|
||||
const currentOperation = useMapper<string, string>(
|
||||
() => Object.values(vm.value?.current_operations ?? {})[0],
|
||||
{
|
||||
() => ({
|
||||
start: t('operation:start'),
|
||||
start_on: t('operation:start-on-host'),
|
||||
pause: t('operation:pause'),
|
||||
@@ -36,7 +36,7 @@ export function useVmOperation(rawVm: MaybeRefOrGetter<XenApiVm | undefined>) {
|
||||
export: t('operation:export'),
|
||||
import: t('operation:import'),
|
||||
unknown: '',
|
||||
},
|
||||
}),
|
||||
'unknown'
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums.ts'
|
||||
import type { XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
|
||||
import type { IconName } from '@core/icons'
|
||||
import { useMapper } from '@core/packages/mapper'
|
||||
import { toComputed } from '@core/utils/to-computed.util.ts'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export function useVmUtils(rawVm: MaybeRefOrGetter<XenApiVm>) {
|
||||
const { t } = useI18n()
|
||||
|
||||
const vm = toComputed(rawVm)
|
||||
|
||||
const powerState = useMapper<VM_POWER_STATE, { icon: IconName; text: string }>(
|
||||
() => vm.value.power_state,
|
||||
() => ({
|
||||
[VM_POWER_STATE.RUNNING]: { icon: 'status:running-circle', text: t('vm:status:running') },
|
||||
[VM_POWER_STATE.HALTED]: { icon: 'status:halted-circle', text: t('vm:status:halted') },
|
||||
[VM_POWER_STATE.PAUSED]: { icon: 'status:paused-circle', text: t('vm:status:paused') },
|
||||
[VM_POWER_STATE.SUSPENDED]: { icon: 'status:suspended-circle', text: t('vm:status:suspended') },
|
||||
}),
|
||||
VM_POWER_STATE.RUNNING
|
||||
)
|
||||
|
||||
return {
|
||||
powerState,
|
||||
}
|
||||
}
|
||||
13
@xen-orchestra/lite/src/route-map.d.ts
vendored
13
@xen-orchestra/lite/src/route-map.d.ts
vendored
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<ComponentStory v-slot="{ properties }" :params="[iconProp().preset('object:host').required()]">
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
iconProp().preset('object:host').required(),
|
||||
prop('size').enum('small', 'medium').preset('medium').widget(),
|
||||
]"
|
||||
>
|
||||
<VtsTreeList>
|
||||
<VtsTreeLoadingItem v-bind="properties" />
|
||||
</VtsTreeList>
|
||||
@@ -8,7 +14,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { iconProp } from '@/libs/story/story-param.ts'
|
||||
import { iconProp, prop } from '@/libs/story/story-param.ts'
|
||||
import VtsTreeList from '@core/components/tree/VtsTreeList.vue'
|
||||
import VtsTreeLoadingItem from '@core/components/tree/VtsTreeLoadingItem.vue'
|
||||
</script>
|
||||
|
||||
@@ -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'),
|
||||
]"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
.preset({ name: '/pool/[uuid]', params: { uuid: '355ee47d-ff4c-4924-3db2-fd86ae629676' } })
|
||||
.widget(),
|
||||
prop('active').bool().widget(),
|
||||
prop('size').enum('small', 'medium').preset('medium').widget(),
|
||||
prop('no-indent').bool().help('Remove indentation based on component depth in the tree'),
|
||||
slot('default'),
|
||||
slot('icon').help('Meant to receive a VtsIcon or another icon component in case the icon prop is not used'),
|
||||
|
||||
@@ -267,7 +267,7 @@ Throw `noSuchObject(id, type)` to get an automatic 404 when the routing doesn't
|
||||
|
||||
## Streaming & file-download routes
|
||||
|
||||
- Obtain and validate the resource **before** setting any response header, so a `noSuchObject` returns a clean JSON 404 instead of a body labelled as a download
|
||||
- Obtain and validate the resource **before** setting any response header, so a `noSuchObject` returns a clean JSON 404 instead of a body labeled as a download
|
||||
- Set `content-disposition: attachment; filename="…"` with a meaningful name built from domain labels (VM / disk / partition). Fold accents to ASCII and collapse anything outside `[\w.-]` so the header and the on-disk filename stay safe (also blocks header injection)
|
||||
- Tie the stream to the request lifetime (`req.on('close', () => stream.destroy())`) so a client disconnect doesn't leak the export
|
||||
- Export: return the stream (`Promise<Readable>`) and let the REST API pipe it to the response. Don't pipe it manually (`pipeline(stream, res)` + `Promise<void>`)
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<UiLoader v-if="busy" v-tooltip="busyTooltip ?? false" />
|
||||
<DisplayIcon v-else-if="icon && tooltip" v-tooltip="tooltip" class="vts-icon" :class="className" :icon />
|
||||
<DisplayIcon v-else-if="icon" class="vts-icon" :class="className" :icon />
|
||||
</template>
|
||||
|
||||
@@ -18,6 +19,7 @@ const { size, name } = defineProps<{
|
||||
name: IconName | undefined
|
||||
busy?: boolean
|
||||
busyTooltip?: string
|
||||
tooltip?: string
|
||||
}>()
|
||||
|
||||
const className = computed(() =>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
:class="{
|
||||
mobile: uiStore.isSmall,
|
||||
'has-panel-column': hasPanelColumn,
|
||||
toggling: isToggling,
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
@@ -11,30 +12,40 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { usePanelStore } from '@core/stores/panel.store.ts'
|
||||
import { PANEL_TRANSITION_DURATION, usePanelStore } from '@core/stores/panel.store.ts'
|
||||
import { useUiStore } from '@core/stores/ui.store.ts'
|
||||
import { computed } from 'vue'
|
||||
import { refAutoReset } from '@vueuse/core'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
const panelStore = usePanelStore()
|
||||
const uiStore = useUiStore()
|
||||
|
||||
const hasPanelColumn = computed(() => (panelStore.isLocked || panelStore.isExpanded) && !uiStore.isSmall)
|
||||
|
||||
const isToggling = refAutoReset(false, PANEL_TRANSITION_DURATION)
|
||||
|
||||
watch(hasPanelColumn, () => {
|
||||
isToggling.value = true
|
||||
})
|
||||
|
||||
const columnTransition = `grid-template-columns ${PANEL_TRANSITION_DURATION}ms`
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.vts-content-side-panel {
|
||||
--side-panel-width: 40rem;
|
||||
|
||||
&:not(.mobile) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 0;
|
||||
align-items: start;
|
||||
overflow-x: clip;
|
||||
min-height: min-content;
|
||||
transition: grid-template-columns 0.25s;
|
||||
|
||||
&.has-panel-column {
|
||||
grid-template-columns: minmax(0, 1fr) var(--side-panel-width);
|
||||
grid-template-columns: minmax(0, 1fr) v-bind('panelStore.cssWidth');
|
||||
}
|
||||
|
||||
&.toggling {
|
||||
transition: v-bind(columnTransition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<template>
|
||||
<div :class="className" class="vts-layout-sidebar">
|
||||
<div
|
||||
v-if="!ui.isSmall"
|
||||
:class="{ active: sidebar.isResizing }"
|
||||
class="resize-handle"
|
||||
@mousedown="sidebar.startResize"
|
||||
/>
|
||||
<VtsResizeHandle v-if="showResizeHandle" :position="resizeHandlePosition" :resizable="sidebar" />
|
||||
<div v-if="slots.header || (showLock && !ui.isSmall)" class="header">
|
||||
<div v-if="slots.header" class="start">
|
||||
<slot name="header" />
|
||||
@@ -16,6 +11,7 @@
|
||||
content: sidebar.isLocked ? t('action:sidebar-unlock') : t('action:sidebar-lock'),
|
||||
placement: lockTooltipPosition,
|
||||
}"
|
||||
:aria-label="sidebar.isLocked ? t('action:sidebar-unlock') : t('action:sidebar-lock')"
|
||||
accent="brand"
|
||||
size="small"
|
||||
:icon="sidebar.isLocked ? 'fa:thumb-tack-slash' : 'fa:thumb-tack'"
|
||||
@@ -36,6 +32,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import VtsResizeHandle from '@core/components/resize-handle/VtsResizeHandle.vue'
|
||||
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive.ts'
|
||||
import { type SidebarSide, useLeftSidebarStore, useRightSidebarStore } from '@core/packages/sidebar'
|
||||
@@ -66,6 +63,10 @@ const sidebar = computed(() => (side === 'right' ? rightSidebar : leftSidebar))
|
||||
|
||||
const lockTooltipPosition = computed(() => (side === 'left' ? 'right' : 'left'))
|
||||
|
||||
const showResizeHandle = computed(() => !ui.isSmall && sidebar.value.isExpanded)
|
||||
|
||||
const resizeHandlePosition = computed(() => (side === 'left' ? 'end' : 'start'))
|
||||
|
||||
const className = computed(() => [
|
||||
{
|
||||
locked: sidebar.value.isLocked && !ui.isSmall,
|
||||
@@ -154,34 +155,5 @@ const className = computed(() => [
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
inset-block: 0;
|
||||
width: 0.8rem;
|
||||
background-color: transparent;
|
||||
cursor: col-resize;
|
||||
transition: background-color 0.4s;
|
||||
user-select: none;
|
||||
z-index: 1;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: var(--color-neutral-border);
|
||||
transition: background-color 0.05s;
|
||||
}
|
||||
}
|
||||
|
||||
&.side--left {
|
||||
.resize-handle {
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.side--right {
|
||||
.resize-handle {
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!-- v5 -->
|
||||
<template>
|
||||
<div class="vts-side-panel-sticky" :class="{ mobile: uiStore.isSmall }">
|
||||
<VtsResizeHandle v-if="showResizeHandle" position="start" :resizable="panelStore" />
|
||||
<VtsPanel
|
||||
:closable="hasSelection"
|
||||
:close-icon="uiStore.isSmall ? 'fa:angle-left' : 'action:close-cancel-clear'"
|
||||
@@ -42,12 +43,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import VtsPanel from '@core/components/panel/VtsPanel.vue'
|
||||
import VtsResizeHandle from '@core/components/resize-handle/VtsResizeHandle.vue'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive.ts'
|
||||
import { usePanelStore } from '@core/stores/panel.store.ts'
|
||||
import { PANEL_TRANSITION_DURATION, usePanelStore } from '@core/stores/panel.store.ts'
|
||||
import { useUiStore } from '@core/stores/ui.store.ts'
|
||||
import { 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<{
|
||||
@@ -70,6 +73,12 @@ const uiStore = useUiStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const showResizeHandle = computed(() => !uiStore.isSmall && panelStore.isExpanded)
|
||||
|
||||
const transformTransition = `transform ${PANEL_TRANSITION_DURATION}ms`
|
||||
|
||||
provide(IK_INFO_SIZE, 'small')
|
||||
|
||||
watch(
|
||||
[() => hasSelection, () => panelStore.isLocked, () => uiStore.isSmall],
|
||||
([hasSelection]) => {
|
||||
@@ -99,8 +108,8 @@ function handleClose() {
|
||||
}
|
||||
|
||||
.vts-side-panel {
|
||||
width: var(--side-panel-width, 40rem);
|
||||
transition: transform 0.25s;
|
||||
width: v-bind('panelStore.cssWidth');
|
||||
transition: v-bind(transformTransition);
|
||||
|
||||
&:not(.mobile) {
|
||||
--panel-viewport-height: calc(100dvh - 5.6rem);
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div
|
||||
:class="className"
|
||||
class="vts-resize-handle"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
tabindex="0"
|
||||
:aria-label="t('action:resize')"
|
||||
:aria-valuenow="ariaValueNow"
|
||||
:aria-valuemin="resizable.minWidth"
|
||||
:aria-valuemax="ariaValueMax"
|
||||
@mousedown="resizable.startResize"
|
||||
@dblclick="resizable.resetWidth()"
|
||||
@keydown.left.prevent="handleArrowKey('left')"
|
||||
@keydown.right.prevent="handleArrowKey('right')"
|
||||
@keydown.home.prevent="resizable.setWidth(resizable.minWidth)"
|
||||
@keydown.end.prevent="resizable.setWidth(resizable.maxWidth)"
|
||||
@keydown.enter.prevent="resizable.resetWidth()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ResizableWidth } from '@core/composables/resizable-width.composable.ts'
|
||||
import { toVariants } from '@core/utils/to-variants.util.ts'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { position, resizable } = defineProps<{
|
||||
position: 'start' | 'end'
|
||||
resizable: ResizableWidth
|
||||
}>()
|
||||
|
||||
const RESIZE_STEP = 10
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const className = computed(() => toVariants({ position, active: resizable.isResizing }))
|
||||
|
||||
const ariaValueNow = computed(() => Math.round(resizable.width))
|
||||
|
||||
const ariaValueMax = computed(() => Math.round(resizable.maxWidth))
|
||||
|
||||
function handleArrowKey(direction: 'left' | 'right') {
|
||||
const isGrowing = (direction === 'right') === (position === 'end')
|
||||
|
||||
resizable.setWidth(resizable.width + (isGrowing ? RESIZE_STEP : -RESIZE_STEP))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.vts-resize-handle {
|
||||
position: absolute;
|
||||
inset-block: 0;
|
||||
width: 0.8rem;
|
||||
background-color: transparent;
|
||||
cursor: col-resize;
|
||||
transition: background-color 0.4s;
|
||||
user-select: none;
|
||||
z-index: 1;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible,
|
||||
&.active {
|
||||
background-color: var(--color-neutral-border);
|
||||
transition: background-color 0.05s;
|
||||
}
|
||||
|
||||
&.position--start {
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
|
||||
&.position--end {
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<UiTableCell class="vts-button-icon-list-cell" align="center">
|
||||
<div class="buttons">
|
||||
<UiButtonIcon
|
||||
v-for="(button, index) of buttons"
|
||||
:key="index"
|
||||
v-tooltip="button.label"
|
||||
:aria-label="button.label"
|
||||
:icon="button.icon"
|
||||
:disabled="button.disabled"
|
||||
:target-scale="1.5"
|
||||
accent="brand"
|
||||
size="small"
|
||||
@click="button.onClick()"
|
||||
/>
|
||||
</div>
|
||||
</UiTableCell>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
||||
import UiTableCell from '@core/components/ui/table-cell/UiTableCell.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive.ts'
|
||||
import type { IconName } from '@core/icons'
|
||||
|
||||
export type ButtonIconItem = {
|
||||
icon: IconName
|
||||
label: string
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
buttons: ButtonIconItem[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.vts-button-icon-list-cell {
|
||||
width: 8rem;
|
||||
padding: 0.8rem 1.6rem 0.8rem 0.8rem;
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<UiTableCell>
|
||||
<div class="vts-double-link-cell">
|
||||
<UiLink size="medium" :icon :to :href :target :disabled class="link">
|
||||
<UiLink size="medium" :icon :icon-tooltip :to :href :target :disabled class="link">
|
||||
<slot />
|
||||
</UiLink>
|
||||
<UiLink
|
||||
v-if="suffix"
|
||||
size="medium"
|
||||
:icon="suffix.icon"
|
||||
:icon-tooltip="suffix.iconTooltip"
|
||||
:to="suffix.to"
|
||||
:href="suffix.href"
|
||||
:target="suffix.target"
|
||||
@@ -27,9 +28,11 @@ import type { IconName } from '@core/icons'
|
||||
|
||||
export type VtsDoubleLinkCellProps = LinkOptions & {
|
||||
icon?: IconName
|
||||
iconTooltip?: string
|
||||
suffix?: LinkOptions & {
|
||||
label: string
|
||||
icon?: IconName
|
||||
iconTooltip?: string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<UiTableCell>
|
||||
<div class="vts-link-cell">
|
||||
<UiLink size="medium" :icon :busy :busy-tooltip :to :href :target class="link">
|
||||
<UiLink size="medium" :icon :icon-tooltip :busy :busy-tooltip :to :href :target class="link">
|
||||
<slot />
|
||||
<VtsIcon v-if="rightIcon" v-tooltip="rightIcon.tooltip ?? false" :name="rightIcon.icon" size="medium" />
|
||||
</UiLink>
|
||||
@@ -25,6 +25,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
export type VtsLinkCellProps = LinkOptions & {
|
||||
icon?: IconName
|
||||
iconTooltip?: string
|
||||
busy?: boolean
|
||||
busyTooltip?: string
|
||||
rightIcon?: {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<UiTableCell class="vts-rank-cell">
|
||||
<div class="content">
|
||||
<span>{{ rank }}</span>
|
||||
<span v-if="shiftIcon" class="shift">
|
||||
<VtsIcon :name="shiftIcon" size="small" />
|
||||
{{ shiftCount }}
|
||||
</span>
|
||||
</div>
|
||||
</UiTableCell>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
||||
import UiTableCell from '@core/components/ui/table-cell/UiTableCell.vue'
|
||||
import type { IconName } from '@core/icons'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { shift = 0 } = defineProps<{
|
||||
rank: number
|
||||
shift?: number
|
||||
}>()
|
||||
|
||||
const shiftIcon = computed<IconName | undefined>(() => {
|
||||
if (shift > 0) {
|
||||
return 'action:move-up'
|
||||
}
|
||||
|
||||
if (shift < 0) {
|
||||
return 'action:move-down'
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
|
||||
const shiftCount = computed(() => Math.abs(shift))
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.vts-rank-cell {
|
||||
width: 7rem;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.shift {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
color: var(--color-neutral-txt-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,19 +1,25 @@
|
||||
<template>
|
||||
<div class="vts-tree-line">
|
||||
<div :class="className" class="vts-tree-line">
|
||||
<div class="tree-line-vertical" />
|
||||
<div :class="{ right }" class="tree-line-horizontal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
import type { TreeItemLabelSize } from '@core/components/ui/tree-item-label/UiTreeItemLabel.vue'
|
||||
import { toVariants } from '@core/utils/to-variants.util.ts'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { size = 'medium' } = defineProps<{
|
||||
right?: boolean
|
||||
size?: TreeItemLabelSize
|
||||
}>()
|
||||
|
||||
const className = computed(() => toVariants({ size }))
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.vts-tree-line {
|
||||
flex: 0 1 1.5em;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -22,7 +28,6 @@ defineProps<{
|
||||
.tree-line-vertical {
|
||||
width: 0.1rem;
|
||||
background: var(--color-brand-txt-base);
|
||||
height: calc(100% + 0.2rem);
|
||||
}
|
||||
|
||||
.tree-line-horizontal {
|
||||
@@ -34,5 +39,21 @@ defineProps<{
|
||||
background: var(--color-brand-txt-base);
|
||||
}
|
||||
}
|
||||
|
||||
&.size--small {
|
||||
flex: 0 1 1.6rem;
|
||||
|
||||
.tree-line-vertical {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.size--medium {
|
||||
flex: 0 1 2.4rem;
|
||||
|
||||
.tree-line-vertical {
|
||||
height: calc(100% + 0.2rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<li class="vts-tree-loading-item">
|
||||
<li :class="className" class="vts-tree-loading-item">
|
||||
<div class="label-placeholder">
|
||||
<div class="link-placeholder">
|
||||
<template v-if="depth > 1">
|
||||
<VtsTreeLine v-for="i in depth - 1" :key="i" :right="i === depth - 1" />
|
||||
<VtsTreeLine v-for="i in depth - 1" :key="i" :right="i === depth - 1" :size="resolvedSize" />
|
||||
</template>
|
||||
<VtsIcon :name="icon" size="medium" />
|
||||
<div class="loader"> </div>
|
||||
@@ -15,22 +15,33 @@
|
||||
<script lang="ts" setup>
|
||||
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
||||
import VtsTreeLine from '@core/components/tree/VtsTreeLine.vue'
|
||||
import type { TreeItemLabelSize } from '@core/components/ui/tree-item-label/UiTreeItemLabel.vue'
|
||||
import type { IconName } from '@core/icons'
|
||||
import { IK_TREE_LIST_DEPTH } from '@core/utils/injection-keys.util.ts'
|
||||
import { inject, ref } from 'vue'
|
||||
import { TREE_ITEM_SMALL_HEIGHT } from '@core/packages/tree/types.ts'
|
||||
import { IK_TREE_ITEM_LABEL_SIZE, IK_TREE_LIST_DEPTH } from '@core/utils/injection-keys.util.ts'
|
||||
import { toVariants } from '@core/utils/to-variants.util.ts'
|
||||
import { computed, inject, ref } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
const { size } = defineProps<{
|
||||
icon: IconName
|
||||
size?: TreeItemLabelSize
|
||||
}>()
|
||||
|
||||
const depth = inject(IK_TREE_LIST_DEPTH, ref(0))
|
||||
|
||||
const contextSize = inject(IK_TREE_ITEM_LABEL_SIZE, undefined)
|
||||
|
||||
const resolvedSize = computed(() => size ?? contextSize ?? 'medium')
|
||||
|
||||
const className = computed(() => toVariants({ size: resolvedSize.value }))
|
||||
|
||||
const smallHeight = `${TREE_ITEM_SMALL_HEIGHT}px`
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.vts-tree-loading-item {
|
||||
.label-placeholder {
|
||||
display: flex;
|
||||
height: 4rem;
|
||||
background-color: var(--color-neutral-background-primary);
|
||||
|
||||
.link-placeholder {
|
||||
@@ -48,6 +59,14 @@ const depth = inject(IK_TREE_LIST_DEPTH, ref(0))
|
||||
}
|
||||
}
|
||||
|
||||
&.size--small .label-placeholder {
|
||||
height: v-bind(smallHeight);
|
||||
}
|
||||
|
||||
&.size--medium .label-placeholder {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 0.5;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<!-- v7 -->
|
||||
<template>
|
||||
<button :class="classNames" :disabled="busy || isDisabled || lockIcon" class="ui-button" type="button">
|
||||
<VtsIcon :busy :name="leftIcon" class="icon" size="current" />
|
||||
<VtsIcon :name="leftIcon" class="icon" size="current" />
|
||||
<slot />
|
||||
<UiLoader v-if="busy" class="icon" />
|
||||
<VtsIcon v-if="lockIcon" name="fa:lock" size="medium" class="icon" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
||||
import UiLoader from '@core/components/ui/loader/UiLoader.vue'
|
||||
import { useDisabled } from '@core/composables/disabled.composable.ts'
|
||||
import type { IconName } from '@core/icons'
|
||||
import { toVariants } from '@core/utils/to-variants.util.ts'
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- v6 -->
|
||||
<template>
|
||||
<component :is="component" :class="classes" class="ui-link" v-bind="attributes">
|
||||
<VtsIcon :name="icon" :busy :busy-tooltip size="medium" />
|
||||
<VtsIcon :name="icon" :tooltip="iconTooltip" :busy :busy-tooltip size="medium" />
|
||||
<slot />
|
||||
<VtsIcon v-if="attributes.target === '_blank'" name="action:open-in-new-tab" size="medium" class="external-icon" />
|
||||
<VtsIcon v-if="isPrimary" v-tooltip="primaryTooltip" name="status:primary-circle" size="medium" />
|
||||
@@ -19,6 +19,7 @@ const props = defineProps<
|
||||
LinkOptions & {
|
||||
size: 'small' | 'medium'
|
||||
icon?: IconName
|
||||
iconTooltip?: string
|
||||
busy?: boolean
|
||||
busyTooltip?: string
|
||||
isPrimary?: boolean
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<div class="ui-panel-card-title">
|
||||
<div class="header">
|
||||
<div class="title" :class="typoClasses[size]">
|
||||
<UiLink v-if="isLink" :size :icon :to :href :target :disabled>
|
||||
<UiLink v-if="isLink" :size :icon :icon-tooltip :to :href :target :disabled>
|
||||
<slot name="label">{{ label }}</slot>
|
||||
</UiLink>
|
||||
<template v-else>
|
||||
<VtsIcon v-if="icon !== undefined" :name="icon" size="medium" class="icon" />
|
||||
<VtsIcon v-if="icon !== undefined" :name="icon" :tooltip="iconTooltip" size="medium" class="icon" />
|
||||
<slot name="label">{{ label }}</slot>
|
||||
</template>
|
||||
<UiCounter
|
||||
@@ -40,11 +40,12 @@ import { computed } from 'vue'
|
||||
|
||||
type PanelCardTitleSize = 'small' | 'medium'
|
||||
|
||||
const { label, size, icon, id, counter, to, href, target, disabled } = defineProps<
|
||||
const { label, size, icon, iconTooltip, id, counter, to, href, target, disabled } = defineProps<
|
||||
LinkOptions & {
|
||||
size: PanelCardTitleSize
|
||||
label?: string
|
||||
icon?: IconName
|
||||
iconTooltip?: string
|
||||
id?: string
|
||||
counter?: CounterValue | { value: CounterValue; accent: CounterAccent }
|
||||
}
|
||||
@@ -61,7 +62,7 @@ const typoClasses = {
|
||||
medium: 'typo-body-bold',
|
||||
}
|
||||
|
||||
const isLink = computed(() => to !== undefined || href !== undefined)
|
||||
const isLink = computed(() => to !== undefined || href !== undefined || disabled)
|
||||
|
||||
const counterProps = computed(() => {
|
||||
if (counter === undefined) {
|
||||
|
||||
@@ -162,7 +162,6 @@ const progress = computed(() => {
|
||||
--line-width: 0.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 1.6rem;
|
||||
gap: 0.4rem;
|
||||
|
||||
.tree-lines {
|
||||
@@ -195,7 +194,7 @@ const progress = computed(() => {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: 1.6rem;
|
||||
padding: 0.4rem 1.6rem;
|
||||
padding: 0.4rem 1.6rem 0.4rem 0.8rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
<template>
|
||||
<RouterLink v-slot="{ isExactActive, isActive, href, navigate }" :to="route" custom>
|
||||
<div
|
||||
:class="isExactActive ? 'exact-active' : active || isActive ? 'active' : undefined"
|
||||
:class="[className, isExactActive ? 'exact-active' : active || isActive ? 'active' : undefined]"
|
||||
class="ui-tree-item-label"
|
||||
v-bind="attrs"
|
||||
>
|
||||
<template v-if="depth > 1">
|
||||
<VtsTreeLine v-for="i in depth - 1" :key="i" />
|
||||
<VtsTreeLine v-for="i in depth - 1" :key="i" :size="resolvedSize" />
|
||||
</template>
|
||||
<div class="toggle-wrapper">
|
||||
<UiButtonIcon
|
||||
v-if="hasToggle"
|
||||
v-tooltip="isExpanded ? t('action:close') : t('action:open')"
|
||||
:aria-label="isExpanded ? t('action:close') : t('action:open')"
|
||||
class="toggle"
|
||||
accent="brand"
|
||||
:icon="isExpanded ? 'fa:angle-down' : 'fa:angle-right'"
|
||||
@@ -43,23 +44,29 @@ import VtsTreeLine from '@core/components/tree/VtsTreeLine.vue'
|
||||
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive.ts'
|
||||
import type { IconName } from '@core/icons'
|
||||
import { TREE_ITEM_SMALL_HEIGHT } from '@core/packages/tree/types.ts'
|
||||
import {
|
||||
IK_TREE_ITEM_EXPANDED,
|
||||
IK_TREE_ITEM_HAS_CHILDREN,
|
||||
IK_TREE_ITEM_LABEL_SIZE,
|
||||
IK_TREE_LIST_DEPTH,
|
||||
} from '@core/utils/injection-keys.util.ts'
|
||||
import { inject, ref, useAttrs } from 'vue'
|
||||
import { toVariants } from '@core/utils/to-variants.util.ts'
|
||||
import { computed, inject, ref, useAttrs } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
export type TreeItemLabelSize = 'small' | 'medium'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
const { size } = defineProps<{
|
||||
route: RouteLocationRaw
|
||||
icon?: IconName
|
||||
active?: boolean
|
||||
size?: TreeItemLabelSize
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -81,6 +88,14 @@ const hasToggle = inject(IK_TREE_ITEM_HAS_CHILDREN, ref(false))
|
||||
const isExpanded = inject(IK_TREE_ITEM_EXPANDED, ref(true))
|
||||
|
||||
const depth = inject(IK_TREE_LIST_DEPTH, ref(0))
|
||||
|
||||
const contextSize = inject(IK_TREE_ITEM_LABEL_SIZE, undefined)
|
||||
|
||||
const resolvedSize = computed(() => size ?? contextSize ?? 'medium')
|
||||
|
||||
const className = computed(() => toVariants({ size: resolvedSize.value }))
|
||||
|
||||
const smallHeight = `${TREE_ITEM_SMALL_HEIGHT}px`
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
@@ -89,13 +104,10 @@ const depth = inject(IK_TREE_LIST_DEPTH, ref(0))
|
||||
align-items: center;
|
||||
color: var(--color-neutral-txt-primary);
|
||||
background-color: var(--color-neutral-background-primary);
|
||||
border-radius: 0.8rem;
|
||||
gap: 0.4rem;
|
||||
padding: 0 0.8rem;
|
||||
margin-bottom: 0.2rem;
|
||||
|
||||
.toggle-wrapper {
|
||||
width: 2.4rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -105,21 +117,16 @@ const depth = inject(IK_TREE_LIST_DEPTH, ref(0))
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
min-width: 0;
|
||||
padding: 0.8rem 0;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
gap: 1.2rem;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-neutral-txt-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
padding-inline-end: 0.4rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
@@ -138,17 +145,49 @@ const depth = inject(IK_TREE_LIST_DEPTH, ref(0))
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* SIZE VARIANTS */
|
||||
|
||||
&.size--small {
|
||||
height: v-bind(smallHeight);
|
||||
|
||||
.toggle-wrapper {
|
||||
width: 1.6rem;
|
||||
}
|
||||
|
||||
.link {
|
||||
gap: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.size--medium {
|
||||
border-radius: 0.8rem;
|
||||
margin-bottom: 0.2rem;
|
||||
|
||||
.toggle-wrapper {
|
||||
width: 2.4rem;
|
||||
}
|
||||
|
||||
.link {
|
||||
padding: 0.8rem 0;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding-inline-end: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* INTERACTION VARIANTS */
|
||||
|
||||
&:is(.exact-active, .active) {
|
||||
background-color: var(--color-brand-background-selected);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover:not(:has(.addons-wrapper :is(button, a):hover)) {
|
||||
background-color: var(--color-brand-background-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
&:active:not(:has(.addons-wrapper :is(button, a):active)) {
|
||||
background-color: var(--color-brand-background-active);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { ifElse } from '@core/utils/if-else.utils.ts'
|
||||
import { useLocalStorage, useRafFn, useStyleTag, useWindowSize } from '@vueuse/core'
|
||||
import { computed, ref, type UnwrapRef } from 'vue'
|
||||
|
||||
const MIN_WIDTH = 280
|
||||
const MAX_WIDTH_FLOOR = 400
|
||||
const MAX_WIDTH_RATIO = 1 / 3
|
||||
const DEFAULT_WIDTH_RATIO = 1 / 5
|
||||
|
||||
export type ResizableSide = 'left' | 'right'
|
||||
|
||||
export type ResizableWidth = UnwrapRef<ReturnType<typeof useResizableWidth>>
|
||||
|
||||
export function useResizableWidth(side: ResizableSide, storageKey: string) {
|
||||
const customWidth = useLocalStorage<number | null>(storageKey, null, {
|
||||
serializer: {
|
||||
read: value => {
|
||||
const width = Number.parseFloat(value)
|
||||
|
||||
return Number.isFinite(width) ? width : null
|
||||
},
|
||||
write: value => String(value),
|
||||
},
|
||||
})
|
||||
|
||||
const isResizing = ref(false)
|
||||
|
||||
const { width: windowWidth } = useWindowSize()
|
||||
|
||||
const maxWidth = computed(() => Math.max(MAX_WIDTH_FLOOR, windowWidth.value * MAX_WIDTH_RATIO))
|
||||
|
||||
const defaultWidth = computed(() => Math.max(MIN_WIDTH, windowWidth.value * DEFAULT_WIDTH_RATIO))
|
||||
|
||||
const width = computed(() => Math.min(maxWidth.value, customWidth.value ?? defaultWidth.value))
|
||||
|
||||
let initialX: number
|
||||
let initialWidth: number
|
||||
let clientX: number
|
||||
|
||||
function startResize(event: MouseEvent) {
|
||||
clientX = event.clientX
|
||||
initialX = clientX
|
||||
initialWidth = width.value
|
||||
isResizing.value = true
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
function setWidth(value: number) {
|
||||
customWidth.value = Math.min(maxWidth.value, Math.max(MIN_WIDTH, value))
|
||||
}
|
||||
|
||||
function resetWidth() {
|
||||
customWidth.value = null
|
||||
}
|
||||
|
||||
function handleMouseMove(event: MouseEvent) {
|
||||
clientX = event.clientX
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
isResizing.value = false
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
const { load, unload } = useStyleTag('body { cursor: col-resize !important; }', { immediate: false })
|
||||
|
||||
const resizeDelta = () => (side === 'left' ? initialWidth + clientX - initialX : initialWidth + initialX - clientX)
|
||||
|
||||
const { pause, resume } = useRafFn(
|
||||
() => {
|
||||
if (clientX === initialX) {
|
||||
return
|
||||
}
|
||||
|
||||
setWidth(resizeDelta())
|
||||
},
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
ifElse(isResizing, [load, resume], [pause, unload])
|
||||
|
||||
return {
|
||||
width,
|
||||
minWidth: MIN_WIDTH,
|
||||
maxWidth,
|
||||
startResize,
|
||||
setWidth,
|
||||
resetWidth,
|
||||
isResizing,
|
||||
}
|
||||
}
|
||||
@@ -103,9 +103,11 @@ export const locales: Locales = {
|
||||
},
|
||||
}
|
||||
|
||||
export const FALLBACK_LOCALE = 'en'
|
||||
|
||||
export default createI18n({
|
||||
locale: localStorage.getItem('lang') ?? 'en',
|
||||
fallbackLocale: 'en',
|
||||
fallbackLocale: FALLBACK_LOCALE,
|
||||
messages,
|
||||
missing: (locale, key, vm) => {
|
||||
if (!import.meta.env.DEV) {
|
||||
|
||||
@@ -4,10 +4,12 @@ import { faCopy, faSquare as checkboxEmpty, type IconDefinition } from '@fortawe
|
||||
import {
|
||||
faAdd,
|
||||
faArrowCircleRight,
|
||||
faArrowDown,
|
||||
faArrowDownLong,
|
||||
faArrowRight,
|
||||
faArrowRotateLeft,
|
||||
faArrowRotateRight,
|
||||
faArrowUp,
|
||||
faArrowUpLong,
|
||||
faArrowUpRightFromSquare,
|
||||
faBan,
|
||||
@@ -153,6 +155,8 @@ export const actionIcons = defineIconPack({
|
||||
color: 'var(--color-neutral-txt-primary)',
|
||||
rotate: 90,
|
||||
},
|
||||
'move-down': constructIcon(faArrowDown),
|
||||
'move-up': constructIcon(faArrowUp),
|
||||
'import-export': [
|
||||
{
|
||||
icon: faArrowUpLong,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
faCamera,
|
||||
faCircle,
|
||||
faCircleNodes,
|
||||
faCircleNotch,
|
||||
faCity,
|
||||
faClock,
|
||||
faDatabase,
|
||||
@@ -71,6 +72,22 @@ const runningNeutral = defineIcon([
|
||||
},
|
||||
])
|
||||
|
||||
const busy = defineIcon([
|
||||
{
|
||||
icon: faCircle,
|
||||
color: 'var(--color-neutral-background-primary)',
|
||||
translate: [7, 5.5],
|
||||
size: 13,
|
||||
},
|
||||
{
|
||||
icon: faCircleNotch,
|
||||
color: 'var(--color-brand-item-base)',
|
||||
translate: [7, 5.5],
|
||||
size: 10,
|
||||
spin: 1,
|
||||
},
|
||||
])
|
||||
|
||||
const arrowLeft = defineIcon([
|
||||
{
|
||||
icon: faCircle,
|
||||
@@ -150,6 +167,7 @@ export const objectIcons = defineIconPack({
|
||||
'host:disabled': [constructIcon(faServer), ...constructCircleStatus('host-disabled-circle')],
|
||||
'host:warning': [constructIcon(faServer), ...constructCircleStatus('warning-circle')],
|
||||
'host:halted': [constructIcon(faServer), ...constructCircleStatus('halted-circle')],
|
||||
'host:busy': [constructIcon(faServer), { icon: busy }],
|
||||
vm: constructIcon(faDesktop),
|
||||
'vm:unknown': [
|
||||
{
|
||||
@@ -166,6 +184,7 @@ export const objectIcons = defineIconPack({
|
||||
'vm:suspended': [constructIcon(faDesktop), ...constructCircleStatus('suspended-circle')],
|
||||
'vm:warning': [constructIcon(faDesktop), ...constructCircleStatus('warning-circle')],
|
||||
'vm:halted': [constructIcon(faDesktop), ...constructCircleStatus('halted-circle')],
|
||||
'vm:busy': [constructIcon(faDesktop), { icon: busy }],
|
||||
'vm-snapshot': [
|
||||
constructIcon(faDesktop),
|
||||
{
|
||||
@@ -187,6 +206,7 @@ export const objectIcons = defineIconPack({
|
||||
'sr:disabled': [constructIcon(faDatabase), ...constructCircleStatus('disabled')],
|
||||
'sr:partially-connected': [constructIcon(faDatabase), ...constructCircleStatus('warning-circle')],
|
||||
'sr:disconnected': [constructIcon(faDatabase), ...constructCircleStatus('danger-circle')],
|
||||
'sr:busy': [constructIcon(faDatabase), { icon: busy }],
|
||||
vdi: constructIcon(faHdd),
|
||||
'vdi:unknown': [
|
||||
{
|
||||
@@ -202,6 +222,7 @@ export const objectIcons = defineIconPack({
|
||||
'vdi:disabled': [constructIcon(faHdd), ...constructCircleStatus('disabled')],
|
||||
'vdi:warning': [constructIcon(faHdd), ...constructCircleStatus('warning-circle')],
|
||||
'vdi:detached': [constructIcon(faHdd), ...constructCircleStatus('danger-circle')],
|
||||
'vdi:busy': [constructIcon(faHdd), { icon: busy }],
|
||||
'vdi-snapshot': [
|
||||
constructIcon(faHdd),
|
||||
{
|
||||
@@ -240,6 +261,7 @@ export const objectIcons = defineIconPack({
|
||||
'network:connected': [constructIcon(faNetworkWired), ...constructCircleStatus('success-circle')],
|
||||
'network:partially-connected': [constructIcon(faNetworkWired), ...constructCircleStatus('warning-circle')],
|
||||
'network:disconnected': [constructIcon(faNetworkWired), ...constructCircleStatus('danger-circle')],
|
||||
'network:busy': [constructIcon(faNetworkWired), { icon: busy }],
|
||||
br: constructIcon(faBoxesStacked),
|
||||
'br:unknown': [
|
||||
{
|
||||
@@ -255,6 +277,7 @@ export const objectIcons = defineIconPack({
|
||||
'br:disabled': [constructIcon(faBoxesStacked), ...constructCircleStatus('disabled')],
|
||||
'br:warning': [constructIcon(faBoxesStacked), ...constructCircleStatus('warning-circle')],
|
||||
'br:disconnected': [constructIcon(faBoxesStacked), ...constructCircleStatus('danger-circle')],
|
||||
'br:busy': [constructIcon(faBoxesStacked), { icon: busy }],
|
||||
'backup-archive': constructIcon(faArchive),
|
||||
'backup-job': [
|
||||
constructIcon(faArchive),
|
||||
|
||||
@@ -1241,7 +1241,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "وثائق وحدة تحكم SDN",
|
||||
"traffic-rules:format-warning": "خطأ في التكوين: قواعد حركة المرور ليست بالتنسيق المتوقع. يرجى {check-doc} حول كيفية ترحيلها.",
|
||||
"traffic-rules:format-warning:check-doc": "مراجعة أحدث الوثائق",
|
||||
"traffic-rules:info-message": "ترتيب قواعد حركة المرور قيد التنفيذ. قد يكون السلوك غير مضمون.",
|
||||
"traffic-rules:no-traffic-rules-detected": "لم يتم اكتشاف أي قواعد حركة مرور",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "يتطلب وضع إضافة XAPI",
|
||||
|
||||
@@ -1204,7 +1204,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "dokumentace k řadiči SDN sítě",
|
||||
"traffic-rules:format-warning": "Chyba nastavení: Pravidla pro provoz nejsou v očekávaném formátu. {check-doc} ohledně toho, jak je převést.",
|
||||
"traffic-rules:format-warning:check-doc": "Podívejte se do neaktuálnější dokumentace",
|
||||
"traffic-rules:info-message": "Implementování pořadí pravidel pro provoz teprve probíhá. Chování může být nebezpečné.",
|
||||
"traffic-rules:no-traffic-rules-detected": "Nezjištěna žádná pravidla pro provoz",
|
||||
"traffic-rules:xapi-plugin-required": "Vyžaduje režim XAPI zásuvného modulu",
|
||||
"translation-tool": "Nástroj pro překládání",
|
||||
|
||||
@@ -1241,7 +1241,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "dokumentationen til SDN-controlleren",
|
||||
"traffic-rules:format-warning": "Konfigurationsfejl: Trafikreglerne er ikke i det forventede format. Du bedes {check-doc} for, hvordan de migreres.",
|
||||
"traffic-rules:format-warning:check-doc": "tjekke den seneste dokumentation",
|
||||
"traffic-rules:info-message": "Rækkefølge for trafikregler er under implementering. Adfærden kan være uforudsigelig.",
|
||||
"traffic-rules:no-traffic-rules-detected": "Ingen trafikregler registreret",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "Kræver XAPI-plugin-tilstand",
|
||||
|
||||
@@ -887,7 +887,6 @@
|
||||
"total-used:": "Gesamt genutzt:",
|
||||
"traffic-rule": "Traffic Regel",
|
||||
"traffic-rules": "Traffic Regeln",
|
||||
"traffic-rules:info-message": "Reihenfolge der Traffic Regeln wird implementiert. Es kann zu unvorgesehenen Verhalten führen.",
|
||||
"traffic-rules:no-traffic-rules-detected": "Keine Traffic Regeln erkannt",
|
||||
"translation-tool": "Übersetzungstool",
|
||||
"trigger": "Trigger",
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"action:edit-br": "@:action:edit BR",
|
||||
"action:edit-config": "Edit config",
|
||||
"action:edit-n-brs": "@:action:edit {n} BRs",
|
||||
"action:edit-rules-order": "@:action:edit rules order within network",
|
||||
"action:edit-traffic-rule": "@:action:edit @.lower:traffic-rule",
|
||||
"action:emergency-shutdown": "Emergency shutdown",
|
||||
"action:emergency-shutdown-host": "@:action:emergency-shutdown host",
|
||||
@@ -123,7 +124,9 @@
|
||||
"action:migrate": "Migrate",
|
||||
"action:migrate-n-vms": "Migrate 1 VM | Migrate {n} VMs",
|
||||
"action:migrate-vdi-on-sr": "Migrate on SR",
|
||||
"action:move-down": "Move down",
|
||||
"action:move-filters-to-parent-group": "Move filters to parent group",
|
||||
"action:move-up": "Move up",
|
||||
"action:open": "Open",
|
||||
"action:open-app": "@:action:open {name}",
|
||||
"action:open-console-in-new-tab": "Open console in new tab",
|
||||
@@ -138,6 +141,7 @@
|
||||
"action:reset-all-networks": "Reset all networks",
|
||||
"action:reset-all-srs": "Reset all SRs",
|
||||
"action:reset-to-default": "Reset to default",
|
||||
"action:resize": "Resize",
|
||||
"action:restart-toolstack": "Restart toolstack",
|
||||
"action:resume": "Resume",
|
||||
"action:revert-to-snapshot": "Revert to snapshot",
|
||||
@@ -283,6 +287,7 @@
|
||||
"bond-mode": "Bond mode",
|
||||
"bond-mode-required": "Bond mode is required.",
|
||||
"bond-status": "Bond status",
|
||||
"bonded-network": "Bonded network",
|
||||
"boot-firmware": "Boot firmware",
|
||||
"bootable": "Bootable",
|
||||
"br-details": "Backup repository details",
|
||||
@@ -537,6 +542,7 @@
|
||||
"interface": "Interface",
|
||||
"interface-required": "Interface is required.",
|
||||
"interfaces": "Interfaces",
|
||||
"internal-network": "Internal network",
|
||||
"interrupted": "Interrupted",
|
||||
"invalid-field": "Invalid field",
|
||||
"invalid-ip-addresses": "One or more invalid IP addresses",
|
||||
@@ -571,6 +577,7 @@
|
||||
"job:arg:missing-payload": "Payload is missing",
|
||||
"job:arg:name-required": "@:name-required",
|
||||
"job:arg:nbd-and-insecure-nbd-exclusive": "NBD and insecure NBD cannot be enabled together",
|
||||
"job:arg:network-required": "@:network-required",
|
||||
"job:arg:password-required": "Password is required",
|
||||
"job:arg:pif-id-required": "PIF ID is required",
|
||||
"job:arg:pif-ids-required": "At least one PIF ID is required",
|
||||
@@ -687,6 +694,8 @@
|
||||
"job:sr-delete:missing-sr": "No SRs to delete",
|
||||
"job:sr-disconnect:missing-sr": "No SRs to disconnect",
|
||||
"job:traffic-rule-delete:missing-rule": "No traffic rule selected",
|
||||
"job:traffic-rule-edit-order:no-free-priority": "No free priority left on this network to apply this order",
|
||||
"job:traffic-rule-edit-order:not-enough-rules": "Requires at least {n} rules",
|
||||
"job:vbd-connect:missing-vbd": "No VBDs to connect",
|
||||
"job:vbd-connect:vbd-already-connected": "VBD is already connected",
|
||||
"job:vbd-delete:missing-vbd": "No VBDs to delete",
|
||||
@@ -1026,6 +1035,7 @@
|
||||
"ok": "OK",
|
||||
"on": "On",
|
||||
"on-object": "on {object}",
|
||||
"operation-in-progress": "{operation}…",
|
||||
"operation:clean-reboot": "Rebooting",
|
||||
"operation:clean-shutdown": "Shutting down",
|
||||
"operation:destroy": "Deleting",
|
||||
@@ -1076,6 +1086,7 @@
|
||||
"pbd-details": "Physical Block Device (PBD) details",
|
||||
"pending": "Pending",
|
||||
"physical-interface-status": "Physical interface status",
|
||||
"physical-network": "Physical network",
|
||||
"pif": "PIF",
|
||||
"pif-status": "PIF status",
|
||||
"pifs": "PIFs",
|
||||
@@ -1127,6 +1138,8 @@
|
||||
"power-on-mode": "Power on mode",
|
||||
"power-on-vm-for-console": "Power on your VM to access its console",
|
||||
"power-state": "Power state",
|
||||
"priority": "Priority",
|
||||
"priority-within": "Priority within {name}",
|
||||
"pro-support": "{name} pro support",
|
||||
"professional-support": "Professional support",
|
||||
"progress": "Progress",
|
||||
@@ -1364,6 +1377,7 @@
|
||||
"theme-solarized-description": "Precision colors for reduced eye strain",
|
||||
"third-parties": "Third party apps",
|
||||
"this-host": "This host",
|
||||
"this-rule": "This rule",
|
||||
"this-vm-cant-be-migrated": "This VM can't be migrated",
|
||||
"timeout": "Timeout",
|
||||
"timezone": "Timezone",
|
||||
@@ -1387,14 +1401,18 @@
|
||||
"traffic-rule": "Traffic rule",
|
||||
"traffic-rule-delete-warning": "This action is irreversible. The selected traffic rule will be permanently deleted.",
|
||||
"traffic-rules": "Traffic rules",
|
||||
"traffic-rules:edit-order-info": "Traffic rules are evaluated by priority, starting with rule 1, then rule 2, and so on. The first matching rule is applied, and subsequent rules are skipped. Priority only matters within the same network.",
|
||||
"traffic-rules:edit-order-save-info": "Changes will only take effect when you save.",
|
||||
"traffic-rules:error-plugin": "You are using the direct OpenFlow protocol (legacy mode), which does not support network traffic rules. To create them, switch to the XAPI plugin mode (beta) — see {check-doc}.",
|
||||
"traffic-rules:error-plugin:check-doc": "the SDN controller documentation",
|
||||
"traffic-rules:format-warning": "Configuration error: Traffic rules are not in the expected format. Please {check-doc} on how to migrate them.",
|
||||
"traffic-rules:format-warning:check-doc": "check the latest documentation",
|
||||
"traffic-rules:info-message": "Traffic rules order is being implemented. Behaviour may be hazardous.",
|
||||
"traffic-rules:no-traffic-rules-detected": "No traffic rules detected",
|
||||
"traffic-rules:position-on-network": "{position} rule on this network.",
|
||||
"traffic-rules:rule-ordinal-suffixes": "one:st, two:nd, few:rd, other:th",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "Requires XAPI plugin mode",
|
||||
"traffic-rules:xapi-plugin-required": "Requires @:xapi-plugin mode",
|
||||
"traffic-rules:xapi-plugin-required-with-link": "Requires {xapi-plugin} mode",
|
||||
"translation-tool": "Translation tool",
|
||||
"treeview": "Treeview",
|
||||
"trigger": "Trigger",
|
||||
@@ -1546,6 +1564,7 @@
|
||||
"with-memory": "With memory",
|
||||
"write": "Write",
|
||||
"writing-speed": "Writing speed",
|
||||
"xapi-plugin": "XAPI plugin",
|
||||
"xcp-ng": "XCP-ng",
|
||||
"xen-orchestra": "Xen Orchestra",
|
||||
"xo": "XO",
|
||||
|
||||
@@ -1241,7 +1241,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "مستندات کنترلر SDN",
|
||||
"traffic-rules:format-warning": "خطای پیکربندی: قوانین ترافیک در قالب مورد انتظار نیستند. لطفاً برای نحوه مهاجرت آنها {check-doc}.",
|
||||
"traffic-rules:format-warning:check-doc": "آخرین مستندات را بررسی کنید",
|
||||
"traffic-rules:info-message": "ترتیب قوانین ترافیک در حال پیادهسازی است. رفتار ممکن است مخاطرهآمیز باشد.",
|
||||
"traffic-rules:no-traffic-rules-detected": "هیچ قانون ترافیکی شناسایی نشد",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "نیازمند حالت افزونه XAPI",
|
||||
|
||||
@@ -1241,7 +1241,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "SDN-ohjaimen dokumentaatio",
|
||||
"traffic-rules:format-warning": "Määritysvirhe: liikennesäännöt eivät ole odotetussa muodossa. {check-doc}, miten ne siirretään.",
|
||||
"traffic-rules:format-warning:check-doc": "Tarkista uusimmasta dokumentaatiosta",
|
||||
"traffic-rules:info-message": "Liikennesääntöjen järjestystä ollaan toteuttamassa. Toiminta voi olla arvaamatonta.",
|
||||
"traffic-rules:no-traffic-rules-detected": "Liikennesääntöjä ei havaittu",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "Vaatii XAPI-lisäosatilan",
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"action:edit-br": "@:action:edit le BR",
|
||||
"action:edit-config": "Modifier config",
|
||||
"action:edit-n-brs": "@:action:edit {n} BRs",
|
||||
"action:edit-rules-order": "@:action:edit l'ordre des règles dans le réseau",
|
||||
"action:edit-traffic-rule": "@:action:edit la @.lower:traffic-rule",
|
||||
"action:emergency-shutdown": "Arrêt d'urgence",
|
||||
"action:emergency-shutdown-host": "@:action:emergency-shutdown de l'hôte",
|
||||
@@ -123,7 +124,9 @@
|
||||
"action:migrate": "Migrer",
|
||||
"action:migrate-n-vms": "Migrer 1 VM | Migrer {n} VMs",
|
||||
"action:migrate-vdi-on-sr": "Migrer sur le SR",
|
||||
"action:move-down": "Descendre",
|
||||
"action:move-filters-to-parent-group": "Déplacer les filtres vers le groupe parent",
|
||||
"action:move-up": "Monter",
|
||||
"action:open": "Ouvrir",
|
||||
"action:open-app": "@:action:open {name}",
|
||||
"action:open-console-in-new-tab": "Ouvrir la console dans un nouvel onglet",
|
||||
@@ -138,6 +141,7 @@
|
||||
"action:reset-all-networks": "Réinitialiser tous les réseaux",
|
||||
"action:reset-all-srs": "Réinitialiser tous les SRs",
|
||||
"action:reset-to-default": "Rétablir la valeur par défaut",
|
||||
"action:resize": "Redimensionner",
|
||||
"action:restart-toolstack": "Redémarrer le toolstack",
|
||||
"action:resume": "Reprendre",
|
||||
"action:revert-to-snapshot": "Revenir à l'instantané",
|
||||
@@ -283,6 +287,7 @@
|
||||
"bond-mode": "Mode de liaison",
|
||||
"bond-mode-required": "Le mode de liaison est requis.",
|
||||
"bond-status": "Statut du bond",
|
||||
"bonded-network": "Réseau lié",
|
||||
"boot-firmware": "Firmware de démarrage",
|
||||
"bootable": "Amorçable",
|
||||
"br-details": "Détails du BR",
|
||||
@@ -537,6 +542,7 @@
|
||||
"interface": "Interface",
|
||||
"interface-required": "L'interface est requise.",
|
||||
"interfaces": "Interfaces",
|
||||
"internal-network": "Réseau interne",
|
||||
"interrupted": "Interrompu",
|
||||
"invalid-field": "Champ invalide",
|
||||
"invalid-ip-addresses": "Une ou plusieurs adresses IPs invalides",
|
||||
@@ -571,6 +577,7 @@
|
||||
"job:arg:missing-payload": "Aucune donnée fournie pour l'execution du job",
|
||||
"job:arg:name-required": "@:name-required",
|
||||
"job:arg:nbd-and-insecure-nbd-exclusive": "NBD et NBD non sécurisé ne peuvent pas être activés ensemble",
|
||||
"job:arg:network-required": "@:network-required",
|
||||
"job:arg:password-required": "Le mot de passe est obligatoire",
|
||||
"job:arg:pif-id-required": "L'ID de PIF est obligatoire",
|
||||
"job:arg:pif-ids-required": "Au moins un ID de PIF est obligatoire",
|
||||
@@ -687,6 +694,8 @@
|
||||
"job:sr-delete:missing-sr": "Aucun SR à supprimer",
|
||||
"job:sr-disconnect:missing-sr": "Aucun SR à déconnecter",
|
||||
"job:traffic-rule-delete:missing-rule": "Aucune règle de trafic sélectionnée",
|
||||
"job:traffic-rule-edit-order:no-free-priority": "Plus aucune priorité libre sur ce réseau pour appliquer cet ordre",
|
||||
"job:traffic-rule-edit-order:not-enough-rules": "Nécessite au moins {n} règles",
|
||||
"job:vbd-connect:missing-vbd": "Aucun VBD à connecter",
|
||||
"job:vbd-connect:vbd-already-connected": "Le VBD est déjà connecté",
|
||||
"job:vbd-delete:missing-vbd": "Aucun VBD à supprimer",
|
||||
@@ -1026,6 +1035,7 @@
|
||||
"ok": "OK",
|
||||
"on": "Allumé",
|
||||
"on-object": "sur {object}",
|
||||
"operation-in-progress": "{operation}…",
|
||||
"operation:clean-reboot": "Redémarrage",
|
||||
"operation:clean-shutdown": "Arrêt",
|
||||
"operation:destroy": "Suppression",
|
||||
@@ -1076,6 +1086,7 @@
|
||||
"pbd-details": "Détails du périphérique de bloc physique (PBD)",
|
||||
"pending": "En cours",
|
||||
"physical-interface-status": "Statut de l'interface physique",
|
||||
"physical-network": "Réseau physique",
|
||||
"pif": "PIF",
|
||||
"pif-status": "Statut du PIF",
|
||||
"pifs": "PIFs",
|
||||
@@ -1127,6 +1138,8 @@
|
||||
"power-on-mode": "Mode d'alimentation",
|
||||
"power-on-vm-for-console": "Allumez votre VM pour accéder à sa console",
|
||||
"power-state": "État d'alimentation",
|
||||
"priority": "Priorité",
|
||||
"priority-within": "Priorité dans {name}",
|
||||
"pro-support": "Support pro {name}",
|
||||
"professional-support": "Support professionnel",
|
||||
"progress": "Progression",
|
||||
@@ -1364,6 +1377,7 @@
|
||||
"theme-solarized-description": "Couleurs précises pour réduire la fatigue oculaire",
|
||||
"third-parties": "Apps tierces",
|
||||
"this-host": "Cet hôte",
|
||||
"this-rule": "Cette règle",
|
||||
"this-vm-cant-be-migrated": "Cette VM ne peut pas être migrée",
|
||||
"timeout": "Délai d'attente",
|
||||
"timezone": "Fuseau horaire",
|
||||
@@ -1387,14 +1401,18 @@
|
||||
"traffic-rule": "Règle de trafic",
|
||||
"traffic-rule-delete-warning": "Cette action est irréversible. La règle de trafic sélectionnée sera définitivement supprimée.",
|
||||
"traffic-rules": "Règles de trafic",
|
||||
"traffic-rules:edit-order-info": "Les règles de trafic sont évaluées par priorité, en commençant par la règle 1, puis la règle 2, et ainsi de suite. La première règle correspondante est appliquée et les suivantes sont ignorées. La priorité ne compte qu'au sein d'un même réseau.",
|
||||
"traffic-rules:edit-order-save-info": "Les modifications ne prendront effet qu'à l'enregistrement.",
|
||||
"traffic-rules:error-plugin": "Vous utilisez le protocole OpenFlow direct (mode historique), qui ne prend pas en charge les règles de trafic réseau. Pour en créer, passez en mode plugin XAPI (bêta) — voir {check-doc}.",
|
||||
"traffic-rules:error-plugin:check-doc": "la documentation du contrôleur SDN",
|
||||
"traffic-rules:format-warning": "Erreur de configuration : les règles de trafic ne sont pas au format attendu. Veuillez {check-doc} la plus récente pour savoir comment les migrer.",
|
||||
"traffic-rules:format-warning:check-doc": "consulter la documentation",
|
||||
"traffic-rules:info-message": "Les règles de trafic sont en cours d'application. Certains comportements peuvent être hasardeux.",
|
||||
"traffic-rules:no-traffic-rules-detected": "Aucune règle de trafic détectée",
|
||||
"traffic-rules:position-on-network": "{position} règle sur ce réseau.",
|
||||
"traffic-rules:rule-ordinal-suffixes": "one:re, other:e",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "Nécessite le mode plugin XAPI",
|
||||
"traffic-rules:xapi-plugin-required": "Nécessite le mode @:xapi-plugin",
|
||||
"traffic-rules:xapi-plugin-required-with-link": "Nécessite le mode {xapi-plugin}",
|
||||
"translation-tool": "Outil de traduction",
|
||||
"treeview": "Arborescence",
|
||||
"trigger": "Déclencheur",
|
||||
@@ -1546,6 +1564,7 @@
|
||||
"with-memory": "Avec mémoire",
|
||||
"write": "Écriture",
|
||||
"writing-speed": "Vitesse d'écriture",
|
||||
"xapi-plugin": "plugin XAPI",
|
||||
"xcp-ng": "XCP-ng",
|
||||
"xen-orchestra": "Xen Orchestra",
|
||||
"xo": "XO",
|
||||
|
||||
@@ -1241,7 +1241,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "la documentazione del controller SDN",
|
||||
"traffic-rules:format-warning": "Errore di configurazione: le regole di traffico non sono nel formato previsto. {check-doc} su come migrarle.",
|
||||
"traffic-rules:format-warning:check-doc": "Consulta la documentazione più recente",
|
||||
"traffic-rules:info-message": "L'ordine delle regole di traffico è in fase di implementazione. Il comportamento potrebbe essere imprevedibile.",
|
||||
"traffic-rules:no-traffic-rules-detected": "Nessuna regola di traffico rilevata",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "Richiede la modalità plugin XAPI",
|
||||
|
||||
@@ -1241,7 +1241,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "SDN コントローラーのドキュメント",
|
||||
"traffic-rules:format-warning": "設定エラー: トラフィックルールが想定された形式ではありません。移行方法については{check-doc}。",
|
||||
"traffic-rules:format-warning:check-doc": "最新のドキュメントを確認してください",
|
||||
"traffic-rules:info-message": "トラフィックルールの順序は実装中です。動作が不安定な場合があります。",
|
||||
"traffic-rules:no-traffic-rules-detected": "トラフィックルールは検出されませんでした",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "XAPI プラグインモードが必要です",
|
||||
|
||||
@@ -1241,7 +1241,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "SDN 컨트롤러 문서",
|
||||
"traffic-rules:format-warning": "구성 오류: 트래픽 규칙이 예상된 형식이 아닙니다. 마이그레이션 방법은 {check-doc}.",
|
||||
"traffic-rules:format-warning:check-doc": "최신 문서를 확인하세요",
|
||||
"traffic-rules:info-message": "트래픽 규칙 순서 기능을 구현 중입니다. 동작이 불안정할 수 있습니다.",
|
||||
"traffic-rules:no-traffic-rules-detected": "감지된 트래픽 규칙 없음",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "XAPI 플러그인 모드 필요",
|
||||
|
||||
@@ -1241,7 +1241,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "dokumentasjonen for SDN-kontrolleren",
|
||||
"traffic-rules:format-warning": "Konfigurasjonsfeil: Trafikkreglene har ikke forventet format. Vennligst {check-doc} om hvordan du migrerer dem.",
|
||||
"traffic-rules:format-warning:check-doc": "se den nyeste dokumentasjonen",
|
||||
"traffic-rules:info-message": "Rekkefølge for trafikkregler er under implementering. Oppførselen kan være uforutsigbar.",
|
||||
"traffic-rules:no-traffic-rules-detected": "Ingen trafikkregler oppdaget",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "Krever XAPI-plugin-modus",
|
||||
|
||||
@@ -1171,7 +1171,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "de documentatie van de SDN-controller",
|
||||
"traffic-rules:format-warning": "Configuratiefout: De verkeersregels hebben niet de verwachte indeling. Raadpleeg {check-doc} voor informatie over hoe je ze kunt migreren.",
|
||||
"traffic-rules:format-warning:check-doc": "raadpleeg de meest recente documentatie",
|
||||
"traffic-rules:info-message": "De verkeersregels worden gehandhaafd. Het gedrag kan gevaarlijk zijn.",
|
||||
"traffic-rules:no-traffic-rules-detected": "Er zijn geen verkeersregels gevonden",
|
||||
"traffic-rules:xapi-plugin-required": "Vereist de XAPI-plug-in modus",
|
||||
"translation-tool": "Vertaalprogramma",
|
||||
|
||||
@@ -1241,7 +1241,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "dokumentację kontrolera SDN",
|
||||
"traffic-rules:format-warning": "Błąd konfiguracji: reguły ruchu nie są w oczekiwanym formacie. {check-doc}, jak je zmigrować.",
|
||||
"traffic-rules:format-warning:check-doc": "Sprawdź w najnowszej dokumentacji",
|
||||
"traffic-rules:info-message": "Kolejność reguł ruchu jest w trakcie implementacji. Działanie może być nieprzewidywalne.",
|
||||
"traffic-rules:no-traffic-rules-detected": "Nie wykryto reguł ruchu",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "Wymaga trybu wtyczki XAPI",
|
||||
|
||||
@@ -1241,7 +1241,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "a documentação do controlador SDN",
|
||||
"traffic-rules:format-warning": "Erro de configuração: as regras de tráfego não estão no formato esperado. Por favor, {check-doc} sobre como migrá-las.",
|
||||
"traffic-rules:format-warning:check-doc": "consulte a documentação mais recente",
|
||||
"traffic-rules:info-message": "A ordem das regras de tráfego está sendo implementada. O comportamento pode ser imprevisível.",
|
||||
"traffic-rules:no-traffic-rules-detected": "Nenhuma regra de tráfego detectada",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "Requer o modo de plugin XAPI",
|
||||
|
||||
@@ -1241,7 +1241,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "документацию SDN-контроллера",
|
||||
"traffic-rules:format-warning": "Ошибка конфигурации: правила для трафика имеют неожиданный формат. Пожалуйста, {check-doc} о том, как их мигрировать.",
|
||||
"traffic-rules:format-warning:check-doc": "ознакомьтесь с последней документацией",
|
||||
"traffic-rules:info-message": "Порядок правил для трафика находится в разработке. Поведение может быть непредсказуемым.",
|
||||
"traffic-rules:no-traffic-rules-detected": "Правила для трафика не обнаружены",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "Требуется режим плагина XAPI",
|
||||
|
||||
@@ -1171,7 +1171,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "dokumentácia k radiču SDN siete",
|
||||
"traffic-rules:format-warning": "Chyba nastavenia: Pravidlá pre prevádzku nie sú v očakávanom formáte. {check-doc} ohľadom toho, ako ich previesť.",
|
||||
"traffic-rules:format-warning:check-doc": "Pozrite sa do najaktuálnejšej dokumentácie",
|
||||
"traffic-rules:info-message": "Implementácia poradia pravidiel pre prevádzku ešte prebieha. Chovanie môže byť nebezpečné.",
|
||||
"traffic-rules:no-traffic-rules-detected": "Nezjištěná žiadna pravidla pre prevádzku",
|
||||
"traffic-rules:xapi-plugin-required": "Vyžaduje režim XAPI zásuvného modulu",
|
||||
"translation-tool": "Prekladový nástroj",
|
||||
|
||||
@@ -1090,7 +1090,6 @@
|
||||
"total-used:": "Totalt använt:",
|
||||
"traffic-rule": "Trafikregel",
|
||||
"traffic-rules": "Trafikregler",
|
||||
"traffic-rules:info-message": "Trafikreglernas ordning införs. Beteendet kan vara farligt.",
|
||||
"traffic-rules:no-traffic-rules-detected": "Inga trafikregler upptäckta",
|
||||
"translation-tool": "Översättningsverktyg",
|
||||
"trigger": "Utlösare",
|
||||
|
||||
@@ -1161,7 +1161,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "SDN denetleyicisi belgelerine",
|
||||
"traffic-rules:format-warning": "Yapılandırma hatası: Trafik kuralları beklenen biçimde değil. Bunları nasıl taşıyacağınızı öğrenmek için lütfen {check-doc}.",
|
||||
"traffic-rules:format-warning:check-doc": "en son belgelere bakın",
|
||||
"traffic-rules:info-message": "Trafik kurallarının sırası henüz uygulanıyor. Davranış riskli olabilir.",
|
||||
"traffic-rules:no-traffic-rules-detected": "Trafik kuralı bulunamadı",
|
||||
"traffic-rules:xapi-plugin-required": "XAPI eklenti modu gerekir",
|
||||
"translation-tool": "Çeviri aracı",
|
||||
|
||||
@@ -1241,7 +1241,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "документацію контролера SDN",
|
||||
"traffic-rules:format-warning": "Помилка конфігурації: правила трафіку мають неочікуваний формат. Будь ласка, {check-doc}, щоб дізнатися, як їх мігрувати.",
|
||||
"traffic-rules:format-warning:check-doc": "перегляньте найновішу документацію",
|
||||
"traffic-rules:info-message": "Порядок правил трафіку ще впроваджується. Поведінка може бути непередбачуваною.",
|
||||
"traffic-rules:no-traffic-rules-detected": "Правил трафіку не виявлено",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "Потрібен режим плагіна XAPI",
|
||||
|
||||
@@ -1171,7 +1171,6 @@
|
||||
"traffic-rules:error-plugin:check-doc": "SDN控制器文档",
|
||||
"traffic-rules:format-warning": "配置错误:流量规则格式异常,请参考{check-doc}完成迁移。",
|
||||
"traffic-rules:format-warning:check-doc": "最新文档",
|
||||
"traffic-rules:info-message": "流量规则顺序正在生效,行为可能存在异常。",
|
||||
"traffic-rules:no-traffic-rules-detected": "未检测到流量规则",
|
||||
"traffic-rules:xapi-plugin-required": "需要XAPI插件模式",
|
||||
"translation-tool": "翻译工具",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useResizableWidth } from '@core/composables/resizable-width.composable.ts'
|
||||
import { SIDEBAR_DEFAULTS, type SidebarSide } from '@core/packages/sidebar/types.ts'
|
||||
import { useSidebarResize } from '@core/packages/sidebar/use-sidebar-resize.ts'
|
||||
import { useSidebarResponsiveExpand } from '@core/packages/sidebar/use-sidebar-responsive-expand.ts'
|
||||
import { useUiStore } from '@core/stores/ui.store.ts'
|
||||
import { useLocalStorage, useToggle } from '@vueuse/core'
|
||||
@@ -13,15 +13,14 @@ function createSidebar(side: SidebarSide) {
|
||||
|
||||
const isExpanded = useLocalStorage(`${prefix}.expanded`, defaults.isExpanded)
|
||||
const isLocked = useLocalStorage(`${prefix}.locked`, defaults.isLocked)
|
||||
const width = useLocalStorage(`${prefix}.width`, defaults.width)
|
||||
|
||||
const toggleExpand = useToggle(isExpanded)
|
||||
const toggleLock = useToggle(isLocked)
|
||||
const { startResize, isResizing } = useSidebarResize(side, width)
|
||||
const resizableWidth = useResizableWidth(side, `${prefix}.custom-width`)
|
||||
|
||||
useSidebarResponsiveExpand(isExpanded)
|
||||
|
||||
const cssWidth = computed(() => (uiStore.isSmall ? '100%' : `${width.value}px`))
|
||||
const cssWidth = computed(() => (uiStore.isSmall ? '100%' : `${resizableWidth.width.value}px`))
|
||||
|
||||
const cssLockedOffset = computed(() => (isExpanded.value ? '0' : `-${cssWidth.value}`))
|
||||
|
||||
@@ -36,14 +35,12 @@ function createSidebar(side: SidebarSide) {
|
||||
return {
|
||||
isExpanded,
|
||||
isLocked,
|
||||
width,
|
||||
toggleExpand,
|
||||
toggleLock,
|
||||
startResize,
|
||||
isResizing,
|
||||
cssWidth,
|
||||
cssLockedOffset,
|
||||
cssUnlockedOffset,
|
||||
...resizableWidth,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import type { ResizableWidth } from '@core/composables/resizable-width.composable.ts'
|
||||
|
||||
export const SIDES = ['left', 'right'] as const
|
||||
|
||||
export type SidebarSide = (typeof SIDES)[number]
|
||||
|
||||
export type SidebarState = {
|
||||
export type SidebarState = ResizableWidth & {
|
||||
isExpanded: boolean
|
||||
isLocked: boolean
|
||||
width: number
|
||||
toggleExpand: (value?: boolean) => boolean
|
||||
toggleLock: (value?: boolean) => boolean
|
||||
startResize: (event: MouseEvent) => void
|
||||
isResizing: boolean
|
||||
cssWidth: string
|
||||
cssLockedOffset: string | number
|
||||
cssUnlockedOffset: string | number
|
||||
}
|
||||
|
||||
export const SIDEBAR_DEFAULTS = {
|
||||
left: { isExpanded: true, isLocked: true, width: 350 },
|
||||
right: { isExpanded: false, isLocked: false, width: 350 },
|
||||
} satisfies Record<SidebarSide, Pick<SidebarState, 'isExpanded' | 'isLocked' | 'width'>>
|
||||
left: { isExpanded: true, isLocked: true },
|
||||
right: { isExpanded: false, isLocked: false },
|
||||
} satisfies Record<SidebarSide, Pick<SidebarState, 'isExpanded' | 'isLocked'>>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import type { SidebarSide } from '@core/packages/sidebar/types.ts'
|
||||
import { ifElse } from '@core/utils/if-else.utils.ts'
|
||||
import { useRafFn, useStyleTag } from '@vueuse/core'
|
||||
import { ref, type Ref } from 'vue'
|
||||
|
||||
export function useSidebarResize(side: SidebarSide, width: Ref<number>) {
|
||||
const isResizing = ref(false)
|
||||
|
||||
let initialX: number
|
||||
let initialWidth: number
|
||||
let clientX: number
|
||||
|
||||
function startResize(event: MouseEvent) {
|
||||
clientX = event.clientX
|
||||
initialX = clientX
|
||||
initialWidth = width.value
|
||||
isResizing.value = true
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
function handleMouseMove(event: MouseEvent) {
|
||||
clientX = event.clientX
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
isResizing.value = false
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
const { load, unload } = useStyleTag('body { cursor: col-resize !important; }', { immediate: false })
|
||||
|
||||
const resizeDelta = () => (side === 'left' ? initialWidth + clientX - initialX : initialWidth + initialX - clientX)
|
||||
|
||||
const { pause, resume } = useRafFn(
|
||||
() => (width.value = Math.min(500, window.innerWidth * 0.3, Math.max(200, resizeDelta()))),
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
ifElse(isResizing, [load, resume], [pause, unload])
|
||||
|
||||
return {
|
||||
startResize,
|
||||
isResizing,
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ export type Identifiable = { id: TreeNodeId }
|
||||
|
||||
export type Labeled = { label: string }
|
||||
|
||||
export const TREE_ITEM_SMALL_HEIGHT = 32
|
||||
|
||||
type AcceptableKeys<TData, TAccepted> = {
|
||||
[K in keyof TData]: TData[K] extends TAccepted ? K : never
|
||||
}[keyof TData]
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { useResizableWidth } from '@core/composables/resizable-width.composable.ts'
|
||||
import { useUiStore } from '@core/stores/ui.store.ts'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
export const PANEL_TRANSITION_DURATION = 250
|
||||
|
||||
export const usePanelStore = defineStore('panel', () => {
|
||||
const uiStore = useUiStore()
|
||||
const isExpanded = useLocalStorage('panel.expanded', true)
|
||||
const isLocked = useLocalStorage('panel.locked', true)
|
||||
|
||||
const resizableWidth = useResizableWidth('right', 'panel.custom-width')
|
||||
|
||||
const cssWidth = computed(() => `${resizableWidth.width.value}px`)
|
||||
|
||||
const cssHorizontalOffset = computed(() => (isExpanded.value ? 0 : '100%'))
|
||||
/* Panel closes automatically when it has no content (true on small UI or when not locked) */
|
||||
const syncsOpenStateWithSelection = computed(() => uiStore.isSmall || !isLocked.value)
|
||||
@@ -59,5 +66,7 @@ export const usePanelStore = defineStore('panel', () => {
|
||||
toggleLock,
|
||||
syncWithSelection,
|
||||
cssHorizontalOffset,
|
||||
cssWidth,
|
||||
...resizableWidth,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import VtsButtonIconListCell, { type ButtonIconItem } from '@core/components/table/cells/VtsButtonIconListCell.vue'
|
||||
import { defineColumn } from '@core/packages/table/define-column.ts'
|
||||
import { renderHeadCell } from '@core/tables/helpers/render-head-cell.ts'
|
||||
import type { HeaderConfig } from '@core/tables/types.ts'
|
||||
import { h } from 'vue'
|
||||
|
||||
export type { ButtonIconItem }
|
||||
|
||||
export const useButtonIconListColumn = defineColumn((config?: HeaderConfig) => ({
|
||||
renderHead: () => renderHeadCell(config?.headerLabel),
|
||||
renderBody: (buttons: ButtonIconItem[]) => h(VtsButtonIconListCell, { buttons }),
|
||||
}))
|
||||
@@ -0,0 +1,10 @@
|
||||
import VtsRankCell from '@core/components/table/cells/VtsRankCell.vue'
|
||||
import { defineColumn } from '@core/packages/table/define-column.ts'
|
||||
import { renderHeadCell } from '@core/tables/helpers/render-head-cell.ts'
|
||||
import type { HeaderConfig } from '@core/tables/types.ts'
|
||||
import { h } from 'vue'
|
||||
|
||||
export const useRankColumn = defineColumn((config?: HeaderConfig) => ({
|
||||
renderHead: () => renderHeadCell(config?.headerLabel),
|
||||
renderBody: (rank: number, shift?: number) => h(VtsRankCell, { rank, shift }),
|
||||
}))
|
||||
@@ -1,7 +1,9 @@
|
||||
import { defineColumns } from '@core/packages/table/define-columns.ts'
|
||||
import { useActionColumn } from '@core/tables/column-definitions/action-column.ts'
|
||||
import { useButtonIconListColumn } from '@core/tables/column-definitions/button-icon-list-column.ts'
|
||||
import { useDoubleLinkColumn } from '@core/tables/column-definitions/double-link-column.ts'
|
||||
import { useNumberColumn } from '@core/tables/column-definitions/number-column.ts'
|
||||
import { useLinkColumn } from '@core/tables/column-definitions/link-column.ts'
|
||||
import { useRankColumn } from '@core/tables/column-definitions/rank-column.ts'
|
||||
import { useTagColumn } from '@core/tables/column-definitions/tag-column.ts'
|
||||
import { useTextColumn } from '@core/tables/column-definitions/text-column.ts'
|
||||
import { useTruncatedTextColumn } from '@core/tables/column-definitions/truncated-text-column.ts'
|
||||
@@ -11,7 +13,9 @@ export const useTrafficRulesColumns = defineColumns(() => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
order: useNumberColumn(),
|
||||
network: useLinkColumn({ headerLabel: () => t('network') }),
|
||||
priority: useTextColumn({ headerLabel: () => t('priority') }),
|
||||
rank: useRankColumn({ headerLabel: () => t('priority') }),
|
||||
policy: useTagColumn({ headerLabel: () => t('policy') }),
|
||||
protocol: useTextColumn({
|
||||
headerLabel: () => t('protocol:port'),
|
||||
@@ -23,5 +27,6 @@ export const useTrafficRulesColumns = defineColumns(() => {
|
||||
}),
|
||||
object: useDoubleLinkColumn({ headerLabel: () => t('object') }),
|
||||
actions: useActionColumn({}),
|
||||
moveButtons: useButtonIconListColumn(),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export type VmState = 'running' | 'halted' | 'paused' | 'suspended'
|
||||
|
||||
export type HostState = 'running' | 'halted' | 'warning' | 'disabled' | 'unknown'
|
||||
@@ -1 +0,0 @@
|
||||
export type POWER_STATE = 'running' | 'paused' | 'halted' | 'suspended'
|
||||
@@ -1,5 +1,7 @@
|
||||
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 { TreeItemLabelSize } from '@core/components/ui/tree-item-label/UiTreeItemLabel.vue'
|
||||
import type { ValueFormatter } from '@core/types/chart.ts'
|
||||
import type { ComputedRef, InjectionKey, Ref } from 'vue'
|
||||
|
||||
@@ -13,6 +15,8 @@ export const IK_TREE_ITEM_EXPANDED = Symbol('IK_TREE_ITEM_EXPANDED') as Injectio
|
||||
|
||||
export const IK_TREE_LIST_DEPTH = Symbol('IK_TREE_LIST_DEPTH') as InjectionKey<Ref<number>>
|
||||
|
||||
export const IK_TREE_ITEM_LABEL_SIZE = Symbol('IK_TREE_ITEM_LABEL_SIZE') as InjectionKey<TreeItemLabelSize>
|
||||
|
||||
export const IK_MENU_HORIZONTAL = Symbol('IK_MENU_HORIZONTAL') as InjectionKey<ComputedRef<boolean>>
|
||||
|
||||
export const IK_CLOSE_MENU = Symbol('IK_CLOSE_MENU') as InjectionKey<() => void>
|
||||
@@ -24,3 +28,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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This guide documents the unit-testing conventions used in `@xen-orchestra/web`, as established across the `pool`, `host` and `vm` modules. Follow it when adding tests for a new module, composable, utility or component.
|
||||
|
||||
**Start with [Assert behaviour, not implementation](./tests/assert-behaviour.md)** — it is the rule the rest of this guide serves.
|
||||
**Start with [Assert behavior, not implementation](./tests/assert-behavior.md)** — it is the rule the rest of this guide serves.
|
||||
|
||||
## What to test first
|
||||
|
||||
@@ -16,12 +16,12 @@ Testing a page is a different job from testing a component, and the component is
|
||||
|
||||
## Getting started
|
||||
|
||||
- [Assert behaviour, not implementation](./tests/assert-behaviour.md) — the one rule that outranks the others
|
||||
- [Assert behavior, not implementation](./tests/assert-behavior.md) — the one rule that outranks the others
|
||||
- [Test runner](./tests/running-tests.md) — Vitest setup and the commands to run a suite
|
||||
|
||||
## Writing a test
|
||||
|
||||
- [File layout and structure](./tests/file-structure.md) — where a test lives, how it is named, how it is organised
|
||||
- [File layout and structure](./tests/file-structure.md) — where a test lives, how it is named, how it is organized
|
||||
- [Setup: prefer helpers over `beforeEach`](./tests/setup-helpers.md) — local helpers, and the shared `use<X>EnhancedData` ones
|
||||
- [Test data factories](./tests/factories.md) — the `src/test/create*` factories and how to use them
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Assert behaviour, not implementation
|
||||
# Assert behavior, not implementation
|
||||
|
||||
**This is the rule the rest of the guide serves.** A test earns its place by failing when behaviour breaks, and staying green through any refactor that preserves it.
|
||||
**This is the rule the rest of the guide serves.** A test earns its place by failing when behavior breaks, and staying green through any refactor that preserves it.
|
||||
|
||||
- Assert concrete outputs and contracts (`input → output`). Never assert _how_ the result was reached — which helper ran, in which order, how many times.
|
||||
- Every test must assert something meaningful. Never re-assert a mock's own return value: `getHostById.mockReturnValue(host)` followed by `expect(…).toBe(host)` tests the mock, not the code.
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
```typescript
|
||||
// ✗ pins a format the code under test does not own — renaming the icon scheme or rewording a
|
||||
// translation then breaks the test even though the behaviour is unchanged
|
||||
// translation then breaks the test even though the behavior is unchanged
|
||||
expect(displayData.vmIcon).toBe('object:vm:running')
|
||||
expect(wrapper.vm.powerState.text).toBe('Running')
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
## Structuring a test file
|
||||
|
||||
- **One `describe` block per function or exposed property**, labelled with its exact name (`describe('getHostInfo')`, `describe('powerState')`). A component test has no such members: group with `describe` only to separate genuinely distinct behaviours, otherwise flat `it`s read better.
|
||||
- **One `describe` block per function or exposed property**, labeled with its exact name (`describe('getHostInfo')`, `describe('powerState')`). A component test has no such members: group with `describe` only to separate genuinely distinct behaviors, otherwise flat `it`s read better.
|
||||
- Use `it` (not `test`).
|
||||
- The name completes the sentence the `it` starts: a **present-tense assertion**, lowercase, no trailing period, and **without a redundant prefix** — the `describe` label already carries the function name. `it('formats the core count with its socket count')` reads as one sentence; `it('getHostCoreSocketInfo formats …')` stutters.
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ it('shows VGA as enabled when the VM uses the "std" adapter', () => {
|
||||
})
|
||||
```
|
||||
|
||||
The test file sits next to the component, named `<ComponentName>.unit.test.ts` (no `.vue`). Group with `describe` only when the component has several distinct behaviours to separate; a small component reads better as flat `it`s.
|
||||
The test file sits next to the component, named `<ComponentName>.unit.test.ts` (no `.vue`). Group with `describe` only when the component has several distinct behaviors to separate; a small component reads better as flat `it`s.
|
||||
|
||||
## Keep components thin
|
||||
|
||||
@@ -40,7 +40,7 @@ The extraction is a refactor, so the tests come **first** and are what proves it
|
||||
3. Re-run those tests **unchanged**.
|
||||
4. Move the ones the extraction made redundant down next to the new util/composable, and delete them from the component test.
|
||||
|
||||
Step 3 is the gate. A test that has to be edited to pass is reporting a behaviour change — say so, do not absorb it into the diff. Tests written only _after_ an extraction describe the new code and cannot catch what the extraction broke, which is the whole risk being taken.
|
||||
Step 3 is the gate. A test that has to be edited to pass is reporting a behavior change — say so, do not absorb it into the diff. Tests written only _after_ an extraction describe the new code and cannot catch what the extraction broke, which is the whole risk being taken.
|
||||
|
||||
Step 4 is selective. A test that only exercised the logic now living in the util moves; a test asserting something the component alone does — rendering, conditional rows, empty states, feeding the util's output into the template — stays.
|
||||
|
||||
@@ -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.
|
||||
@@ -109,7 +107,7 @@ expect(findTableRows(wrapper)).toEqual([
|
||||
|
||||
### Never spell out a translation
|
||||
|
||||
A rendered label or a translated value is wording the component does not own, so [asserting behaviour](./assert-behaviour.md) means naming the **key**, not the English string. `src/test/i18n.ts` exposes the `t` of the very instance `createGlobalTestConfig()` installs:
|
||||
A rendered label or a translated value is wording the component does not own, so [asserting behavior](./assert-behavior.md) means naming the **key**, not the English string. `src/test/i18n.ts` exposes the `t` of the very instance `createGlobalTestConfig()` installs:
|
||||
|
||||
```typescript
|
||||
// ✗ rewording the translation breaks the test although the component still picks the same key
|
||||
@@ -230,7 +228,7 @@ Pushing before mounting is what makes the active tab assertable; without a push
|
||||
There is no layout engine, and Vitest's `css` option is off, so component styles are never loaded. Out of reach:
|
||||
|
||||
- `toBeVisible()` and anything depending on real visibility — assert presence/absence instead (`expect(wrapper.find('.foo').exists()).toBe(false)`)
|
||||
- `getComputedStyle` assertions on design tokens (colour, padding, `flex-direction`)
|
||||
- `getComputedStyle` assertions on design tokens (color, padding, `flex-direction`)
|
||||
- real event dispatch — `trigger()` simulates events
|
||||
- canvas (ECharts), `ResizeObserver`, `IntersectionObserver` — for a chart card, stub the chart (see [Chart cards](#chart-cards))
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ expect(wrapper.vm.powerState.text).toBe(wrapper.vm.t('vm:status:running'))
|
||||
|
||||
The composable result becomes the component state, so `wrapper.vm` exposes it with refs **already unwrapped** — no `.value`. Read through `wrapper.vm` rather than destructuring it: a destructured value is a snapshot and stops following updates.
|
||||
|
||||
Because the real i18n instance is installed, translations resolve to actual strings. Return `t` from the `setup` callback so the assertion can build its expected label with it instead of hard-coding the wording — see [Assert behaviour](./assert-behaviour.md).
|
||||
Because the real i18n instance is installed, translations resolve to actual strings. Return `t` from the `setup` callback so the assertion can build its expected label with it instead of hard-coding the wording — see [Assert behavior](./assert-behavior.md).
|
||||
|
||||
The wrapper is unmounted after the test, so `onUnmounted` and `onScopeDispose` cleanup runs and no watcher of a previous test can re-fire in the next one.
|
||||
|
||||
@@ -57,6 +57,6 @@ expect(wrapper.vm.relativeStartTime).toBe(wrapper.vm.expectedRelativeTime)
|
||||
|
||||
Note the object literal: `setup()` returns the component **bindings**, so the callback must hand back an object. A lone ref is not component state and would not surface on `wrapper.vm`. Anything the assertion needs — the `t`, a `locale`, an expected value — is returned alongside, which is why the callback body is a block rather than a bare object.
|
||||
|
||||
The expected value is **derived the way production derives it** ([Assert behaviour](./assert-behaviour.md)): `getRelativeTime` owns the wording, and what the composable decides is which date to hand it. `expect(typeof …).toBe('string')` would pass for any string, including one built from a start time read as milliseconds instead of seconds — it asserts the type, not the behaviour. Where a value is environment-dependent, name the helper production uses and call it with the same input; never assert merely that the value is non-empty or differs from some other label.
|
||||
The expected value is **derived the way production derives it** ([Assert behavior](./assert-behavior.md)): `getRelativeTime` owns the wording, and what the composable decides is which date to hand it. `expect(typeof …).toBe('string')` would pass for any string, including one built from a start time read as milliseconds instead of seconds — it asserts the type, not the behavior. Where a value is environment-dependent, name the helper production uses and call it with the same input; never assert merely that the value is non-empty or differs from some other label.
|
||||
|
||||
Functions that only touch refs/computed (e.g. `getPowerState`) can be called on the returned object as usual.
|
||||
|
||||
@@ -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,17 +42,13 @@
|
||||
<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" />
|
||||
<UiCollapsibleList tag="ul" :total-items="backedUpVmsCount">
|
||||
<li v-for="vm in backedUpVms" :key="vm.id">
|
||||
<UiLink
|
||||
size="small"
|
||||
:icon="`object:vm:${toLower(vm.power_state)}`"
|
||||
:to="{ name: '/vm/[id]/dashboard', params: { id: vm.id } }"
|
||||
>
|
||||
<li v-for="{ vm, icon, iconTooltip } in backedUpVmLinks" :key="vm.id">
|
||||
<UiLink size="small" :icon :icon-tooltip :to="{ name: '/vm/[id]/dashboard', params: { id: vm.id } }">
|
||||
{{ vm.name_label }}
|
||||
</UiLink>
|
||||
</li>
|
||||
@@ -67,7 +63,10 @@ import BackupJobSmartModePoolItem from '@/modules/backup-job/components/list/pan
|
||||
import BackupJobSmartModeTagsItem from '@/modules/backup-job/components/list/panel/card-items/BackupJobSmartModeTagsItem.vue'
|
||||
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 { useXoVmOperationLabel } from '@/modules/vm/composables/xo-vm-operation-label.composable.ts'
|
||||
import { getVmIcon } from '@/modules/vm/utils/xo-vm.util.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'
|
||||
@@ -76,6 +75,7 @@ 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'
|
||||
import { toLower } from 'lodash-es'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { backedUpVms: rawBackedUpVms } = defineProps<{
|
||||
@@ -86,6 +86,12 @@ const { t } = useI18n()
|
||||
|
||||
const { backedUpVms, backedUpVmsCount, smartModePools, smartModePowerState, smartModeTags, isSmartModeEnabled } =
|
||||
useXoBackedUpVmsUtils(() => rawBackedUpVms)
|
||||
|
||||
const { getVmIconTooltip } = useXoVmOperationLabel()
|
||||
|
||||
const backedUpVmLinks = computed(() =>
|
||||
backedUpVms.value.map(vm => ({ vm, icon: getVmIcon(vm), iconTooltip: getVmIconTooltip(vm) }))
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
@@ -95,10 +101,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;
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'))
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -25,7 +25,9 @@ import type { BackupLogFilterableData } from '@/modules/backup-log/types/xo-back
|
||||
import { getBackupLogSrIds, getBackupLogVmIds } from '@/modules/backup-log/utils/xo-backup-log.util.ts'
|
||||
import { useXoSrUtils } from '@/modules/storage-repository/composables/xo-sr-utils.composable.ts'
|
||||
import { useXoSrCollection } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
|
||||
import { useXoVmOperationLabel } from '@/modules/vm/composables/xo-vm-operation-label.composable.ts'
|
||||
import { useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
|
||||
import { getVmIcon } from '@/modules/vm/utils/xo-vm.util.ts'
|
||||
import { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
|
||||
import VtsQueryBuilder from '@core/components/query-builder/VtsQueryBuilder.vue'
|
||||
import VtsRow from '@core/components/table/VtsRow.vue'
|
||||
@@ -34,12 +36,10 @@ import UiTitle from '@core/components/ui/title/UiTitle.vue'
|
||||
import { usePagination } from '@core/composables/pagination.composable.ts'
|
||||
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
|
||||
import { useTableState } from '@core/composables/table-state.composable.ts'
|
||||
import { objectIcon } from '@core/icons'
|
||||
import { useQueryBuilderSchema } from '@core/packages/query-builder/schema/use-query-builder-schema.ts'
|
||||
import { useQueryBuilderFilter } from '@core/packages/query-builder/use-query-builder-filter.ts'
|
||||
import { useBackupLogColumns } from '@core/tables/column-sets/backup-log-columns.ts'
|
||||
import { useStringSchema } from '@core/utils/query-builder/use-string-schema.ts'
|
||||
import { toLower } from 'lodash-es'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -59,6 +59,8 @@ const { getBackupJobById } = useXoBackupJobCollection()
|
||||
|
||||
const { getVmById } = useXoVmCollection()
|
||||
|
||||
const { getVmIconTooltip } = useXoVmOperationLabel()
|
||||
|
||||
const { getSrById } = useXoSrCollection()
|
||||
|
||||
const { getSrStatusIcon } = useXoSrUtils()
|
||||
@@ -121,7 +123,8 @@ const { HeadCells, BodyCells } = useBackupLogColumns({
|
||||
return r({
|
||||
label: firstVm.value.name_label,
|
||||
to: { name: '/vm/[id]/dashboard', params: { id: firstVm.value.id } },
|
||||
icon: objectIcon('vm', toLower(firstVm.value.power_state)),
|
||||
icon: getVmIcon(firstVm.value),
|
||||
iconTooltip: getVmIconTooltip(firstVm.value),
|
||||
extraCount: vmIds.value.length - 1,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -17,7 +17,7 @@
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<template v-if="vms.length > 0">
|
||||
<VtsCardRowKeyValue v-for="({ id, vm, icon }, index) in vms" :key="id">
|
||||
<VtsCardRowKeyValue v-for="({ id, vm, icon, iconTooltip }, index) in vms" :key="id">
|
||||
<template #key>
|
||||
<template v-if="index === 0">{{ t('vm') }}</template>
|
||||
</template>
|
||||
@@ -26,6 +26,7 @@
|
||||
v-if="vm !== undefined"
|
||||
size="small"
|
||||
:icon
|
||||
:icon-tooltip
|
||||
:to="{ name: '/vm/[id]/dashboard', params: { id: vm.id } }"
|
||||
>
|
||||
{{ vm.name_label }}
|
||||
@@ -85,7 +86,7 @@
|
||||
<VtsStatus :status="backupLog.status" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
</div>
|
||||
</VtsCardRowList>
|
||||
</UiPanelCard>
|
||||
</template>
|
||||
|
||||
@@ -96,17 +97,18 @@ import type { FrontXoBackupLog } from '@/modules/backup-log/remote-resources/use
|
||||
import { getBackupLogSrIds, getBackupLogVmIds } from '@/modules/backup-log/utils/xo-backup-log.util.ts'
|
||||
import { useXoSrUtils } from '@/modules/storage-repository/composables/xo-sr-utils.composable.ts'
|
||||
import { useXoSrCollection } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
|
||||
import { useXoVmOperationLabel } from '@/modules/vm/composables/xo-vm-operation-label.composable.ts'
|
||||
import { useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
|
||||
import { getVmIcon } from '@/modules/vm/utils/xo-vm.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 VtsStatus from '@core/components/status/VtsStatus.vue'
|
||||
import UiInfo from '@core/components/ui/info/UiInfo.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'
|
||||
import { objectIcon } from '@core/icons'
|
||||
import { toLower } from 'lodash-es'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -122,6 +124,8 @@ const { useGetBackupJobById } = useXoBackupJobCollection()
|
||||
|
||||
const { getVmById } = useXoVmCollection()
|
||||
|
||||
const { getVmIconTooltip } = useXoVmOperationLabel()
|
||||
|
||||
const { getSrById } = useXoSrCollection()
|
||||
|
||||
const { getSrStatusIcon } = useXoSrUtils()
|
||||
@@ -144,7 +148,7 @@ const vms = computed(() =>
|
||||
getBackupLogVmIds(backupLog).map(id => {
|
||||
const vm = getVmById(id)
|
||||
|
||||
return { id, vm, icon: vm !== undefined ? objectIcon('vm', toLower(vm.power_state)) : undefined }
|
||||
return { id, vm, icon: vm !== undefined ? getVmIcon(vm) : undefined, iconTooltip: getVmIconTooltip(vm) }
|
||||
})
|
||||
)
|
||||
|
||||
@@ -156,13 +160,3 @@ const srs = computed(() =>
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.backup-log-infos-card {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user