mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-10 22:14:48 -05:00
feat(xo-6/vm): add dashboard view (#8585)
This commit is contained in:
@@ -75,7 +75,7 @@ type BaseXoVm = BaseXapiXo & {
|
||||
isNestedVirtEnabled: boolean
|
||||
hasVendorDevice: boolean
|
||||
high_availability: string
|
||||
installTime?: number
|
||||
installTime?: number | null
|
||||
isFirmwareSupported: boolean
|
||||
memory: {
|
||||
dynamic: number[]
|
||||
@@ -101,7 +101,7 @@ type BaseXoVm = BaseXapiXo & {
|
||||
secureBoot: boolean
|
||||
snapshots: XoVmSnapshot['id'][]
|
||||
startDelay: number
|
||||
startTime?: number
|
||||
startTime?: number | null
|
||||
suspendSr?: XoSr['id']
|
||||
tags: string[]
|
||||
vga?: string
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useTimestamp } from '@vueuse/core'
|
||||
import { computed, type MaybeRefOrGetter, toValue } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export enum SECONDS {
|
||||
MINUTE = 60,
|
||||
HOUR = SECONDS.MINUTE * 60,
|
||||
DAY = SECONDS.HOUR * 24,
|
||||
WEEK = SECONDS.DAY * 7,
|
||||
MONTH = SECONDS.DAY * 30,
|
||||
YEAR = SECONDS.DAY * 365,
|
||||
}
|
||||
|
||||
export type TimeThreshold = [number, Intl.RelativeTimeFormatUnit]
|
||||
|
||||
const timeThresholds: TimeThreshold[] = [
|
||||
[SECONDS.MINUTE, 'second'],
|
||||
[SECONDS.HOUR, 'minute'],
|
||||
[SECONDS.DAY, 'hour'],
|
||||
[SECONDS.WEEK, 'day'],
|
||||
[SECONDS.MONTH, 'week'],
|
||||
[SECONDS.YEAR, 'month'],
|
||||
[Infinity, 'year'],
|
||||
]
|
||||
|
||||
export function useTimeAgo(referenceDate: MaybeRefOrGetter<Date | number | string>) {
|
||||
const { locale } = useI18n()
|
||||
|
||||
const formatter = computed(() => new Intl.RelativeTimeFormat(locale.value, { numeric: 'auto' }))
|
||||
|
||||
const now = useTimestamp({ interval: 1000 })
|
||||
|
||||
const referenceTime = computed(() => new Date(toValue(referenceDate)).getTime())
|
||||
|
||||
const distance = computed(() => {
|
||||
const diff = Math.trunc((referenceTime.value - now.value) / 1000)
|
||||
|
||||
if (Math.abs(diff) < 60) {
|
||||
return Math.trunc(diff / 10) * 10 || 0 // Avoid recomputing when the value changes from 0 to -0
|
||||
}
|
||||
|
||||
return timeThresholds.reduce(
|
||||
(acc, [threshold]) => (Math.abs(acc) < threshold ? acc : Math.trunc(acc / threshold) * threshold),
|
||||
diff
|
||||
)
|
||||
})
|
||||
|
||||
return computed(() => {
|
||||
const index = timeThresholds.findIndex(([threshold]) => Math.abs(distance.value) < threshold)
|
||||
const divisor = index > 0 ? timeThresholds[index - 1][0] : 1
|
||||
return formatter.value.format(Math.trunc(distance.value / divisor), timeThresholds[index][1])
|
||||
})
|
||||
}
|
||||
@@ -130,6 +130,7 @@
|
||||
"cpus": "CPUs",
|
||||
"crash-dump-storage-repository": "Crash dump storage repository",
|
||||
"create": "Create",
|
||||
"created-on": "Created on",
|
||||
"custom-config": "Custom config",
|
||||
"dark-mode.auto": "Auto dark mode",
|
||||
"dark-mode.disable": "Disable dark mode",
|
||||
@@ -210,8 +211,8 @@
|
||||
"graphics-display": "Graphics & Display",
|
||||
"gzip": "gzip",
|
||||
"hardware": "Hardware",
|
||||
"heartbeat-storage-repository": "Heartbeat storage repository",
|
||||
"hardware-specifications": "Hardware specifications",
|
||||
"heartbeat-storage-repository": "Heartbeat storage repository",
|
||||
"here": "Here",
|
||||
"high-availability": "High availability (HA)",
|
||||
"host": "Host",
|
||||
@@ -369,6 +370,7 @@
|
||||
"no-vif-detected": "No VIF detected",
|
||||
"none": "None",
|
||||
"not-found": "Not found",
|
||||
"not-running": "Not running",
|
||||
"not-yet-available": "Information is not yet available",
|
||||
"object": "Object",
|
||||
"object-not-found": "Object {id} can't be found…",
|
||||
@@ -421,6 +423,7 @@
|
||||
"ram": "RAM",
|
||||
"ram-provisioning": "RAM provisioning",
|
||||
"ram-usage": "RAM usage",
|
||||
"read": "Read",
|
||||
"read-only": "Read-only",
|
||||
"reboot": "Reboot",
|
||||
"receive": "Receive",
|
||||
@@ -540,6 +543,7 @@
|
||||
"vcpus": "vCPU | vCPU | vCPUs",
|
||||
"vcpus-assigned": "vCPUs assigned",
|
||||
"vcpus-used": "vCPUs used",
|
||||
"vdi-throughput": "VDI throughput",
|
||||
"vdis": "VDI | VDI | VDIs",
|
||||
"version": "Version",
|
||||
"vga": "VGA",
|
||||
@@ -552,12 +556,17 @@
|
||||
"virtual-tpm": "Virtual TPM (VTPM)",
|
||||
"virtualization-boot-settings": "Virtualization & boot settings",
|
||||
"virtualization-mode": "Virtualization mode",
|
||||
"virtualization-type": "Virtualization type",
|
||||
"vlan": "VLAN",
|
||||
"vm": "VM",
|
||||
"vm-description": "VM description",
|
||||
"vm-is-running": "The VM is running",
|
||||
"vm-limit-topology": "VM limit topology",
|
||||
"vm-management": "VM management",
|
||||
"vm-status.halted": "Halted",
|
||||
"vm-status.paused": "Paused",
|
||||
"vm-status.running": "Running",
|
||||
"vm-status.suspended": "Suspended",
|
||||
"vm.active": "Active",
|
||||
"vm.inactive": "Inactive",
|
||||
"vms": "VM | VMs",
|
||||
@@ -570,6 +579,7 @@
|
||||
"vms-status.suspended": "Suspended",
|
||||
"vms-status.unknown": "Unknown",
|
||||
"vms-status.unknown.tooltip": "For which XO has lost connection to the pool",
|
||||
"write": "Write",
|
||||
"xo-backups": "XO backups",
|
||||
"xo-lite-under-construction": "XOLite is under construction",
|
||||
"xo-replications": "XO replications",
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
"cpus": "CPUs",
|
||||
"crash-dump-storage-repository": "Dépôt de stockage des log de crash",
|
||||
"create": "Créer",
|
||||
"created-on": "Créé le",
|
||||
"custom-config": "Configuration personnalisée",
|
||||
"dark-mode.auto": "Mode sombre automatique",
|
||||
"dark-mode.disable": "Désactiver le mode sombre",
|
||||
@@ -210,8 +211,8 @@
|
||||
"graphics-display": "Graphique & Affichage",
|
||||
"gzip": "gzip",
|
||||
"hardware": "Matériel",
|
||||
"heartbeat-storage-repository": "Dépôt de stockage pour le heartbeat de la haute disponibilité",
|
||||
"hardware-specifications": "Spécifications matérielles",
|
||||
"heartbeat-storage-repository": "Dépôt de stockage pour le heartbeat de la haute disponibilité",
|
||||
"here": "Ici",
|
||||
"high-availability": "Haute Disponibilité (HA)",
|
||||
"host": "Hôte",
|
||||
@@ -369,6 +370,7 @@
|
||||
"no-vif-detected": "Aucun VIF détecté",
|
||||
"none": "Aucun",
|
||||
"not-found": "Non trouvé",
|
||||
"not-running": "Pas en cours d'exécution",
|
||||
"not-yet-available": "Information pas encore disponible",
|
||||
"object": "Objet",
|
||||
"object-not-found": "L'objet {id} est introuvable…",
|
||||
@@ -421,6 +423,7 @@
|
||||
"ram": "RAM",
|
||||
"ram-provisioning": "Provisionnement RAM",
|
||||
"ram-usage": "Utilisation de la RAM",
|
||||
"read": "Lecture",
|
||||
"read-only": "Lecture seule",
|
||||
"reboot": "Redémarrer",
|
||||
"receive": "Recevoir",
|
||||
@@ -540,6 +543,7 @@
|
||||
"vcpus": "vCPU | vCPU | vCPUs",
|
||||
"vcpus-assigned": "vCPUs assignés",
|
||||
"vcpus-used": "vCPUs utilisés",
|
||||
"vdi-throughput": "Débit du VDI",
|
||||
"vdis": "VDI | VDI | VDIs",
|
||||
"version": "Version",
|
||||
"vga": "VGA",
|
||||
@@ -552,12 +556,17 @@
|
||||
"virtual-tpm": "TPM virtuel (VTPM)",
|
||||
"virtualization-boot-settings": "Virtualisation & paramètres de démarrage",
|
||||
"virtualization-mode": "Mode de virtualisation",
|
||||
"virtualization-type": "Type de virtualisation",
|
||||
"vlan": "VLAN",
|
||||
"vm": "VM",
|
||||
"vm-description": "Description de la VM",
|
||||
"vm-is-running": "La VM est en cours d'exécution",
|
||||
"vm-limit-topology": "Limite de topologie physique des VMs",
|
||||
"vm-management": "Gestion de la VM",
|
||||
"vm-status.halted": "Éteinte",
|
||||
"vm-status.paused": "En pause",
|
||||
"vm-status.running": "Démarrée",
|
||||
"vm-status.suspended": "Suspendue",
|
||||
"vm.active": "Active | Active | Actives",
|
||||
"vm.inactive": "Inactive | Inactive | Inactives",
|
||||
"vms": "VM | VMs",
|
||||
@@ -570,6 +579,7 @@
|
||||
"vms-status.suspended": "Suspendues",
|
||||
"vms-status.unknown": "Inconnu",
|
||||
"vms-status.unknown.tooltip": "Dont XO a perdu la connexion avec le pool",
|
||||
"write": "Écriture",
|
||||
"xo-backups": "Sauvegardes XO",
|
||||
"xo-lite-under-construction": "XOLite est en construction",
|
||||
"xo-replications": "Réplications XO",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { parse, Scale, raw } from 'human-format'
|
||||
import { parse, raw, Scale } from 'human-format'
|
||||
|
||||
const scale = Scale.create(['', 'KiB', 'MiB', 'GiB', 'TiB'], 1024)
|
||||
const scale = Scale.create(['B', 'KiB', 'MiB', 'GiB', 'TiB'], 1024)
|
||||
|
||||
export const formatSizeRaw = (bytes: number | undefined, decimals: number) => {
|
||||
if (bytes === undefined) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<VtsTreeItem expanded>
|
||||
<UiTreeItemLabel :route="`/vm/${leaf.data.id}/`" no-indent>
|
||||
<UiTreeItemLabel :route="`/vm/${leaf.data.id}`" no-indent>
|
||||
{{ leaf.data.name_label }}
|
||||
<template #icon>
|
||||
<UiObjectIcon
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
</template>
|
||||
</UiHeadBar>
|
||||
<TabList>
|
||||
<TabItem disabled>{{ t('dashboard') }}</TabItem>
|
||||
<RouterLink v-slot="{ isActive, href }" :to="`/vm/${vm.id}/dashboard`" custom>
|
||||
<TabItem :active="isActive" :href tag="a">
|
||||
{{ t('dashboard') }}
|
||||
</TabItem>
|
||||
</RouterLink>
|
||||
<RouterLink v-slot="{ isActive, href }" :to="`/vm/${vm.id}/console`" custom>
|
||||
<TabItem :active="isActive" :href tag="a">
|
||||
{{ t('console') }}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<UiCard>
|
||||
<UiCardTitle>
|
||||
{{ t('cpu-usage') }}
|
||||
<template #description>{{ t('last-week') }}</template>
|
||||
</UiCardTitle>
|
||||
<VtsLoadingHero v-if="loading" type="card" />
|
||||
<VtsErrorNoDataHero v-else-if="error" type="card" />
|
||||
<VtsNoDataHero v-else-if="cpuUsage.length === 0" type="card" />
|
||||
<VtsLinearChart v-else :data="cpuUsage" :max-value :value-formatter />
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { XoVmStats } from '@/types/xo/vm-stats.type.ts'
|
||||
import type { LinearChartData, ValueFormatter } from '@core/types/chart.ts'
|
||||
import VtsErrorNoDataHero from '@core/components/state-hero/VtsErrorNoDataHero.vue'
|
||||
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
|
||||
import VtsNoDataHero from '@core/components/state-hero/VtsNoDataHero.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
|
||||
import { computed, defineAsyncComponent } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { data } = defineProps<{
|
||||
data: XoVmStats | null
|
||||
loading: boolean
|
||||
error?: string
|
||||
}>()
|
||||
|
||||
const VtsLinearChart = defineAsyncComponent(() => import('@core/components/linear-chart/VtsLinearChart.vue'))
|
||||
|
||||
const { t, n } = useI18n()
|
||||
|
||||
const cpuUsage = computed<LinearChartData>(() => {
|
||||
if (!data?.stats.cpus) {
|
||||
return []
|
||||
}
|
||||
|
||||
const cpus = Object.values(data.stats.cpus)
|
||||
|
||||
const result = new Map<number, { timestamp: number; value: number }>()
|
||||
|
||||
const timestampStart = data.endTimestamp - data.interval * (cpus[0].length - 1)
|
||||
|
||||
for (let hourIndex = 0; hourIndex < cpus[0].length; hourIndex++) {
|
||||
const timestamp = (timestampStart + hourIndex * data.interval) * 1000
|
||||
|
||||
const cpuUsageSum = cpus.reduce((total, cpu) => total + cpu[hourIndex], 0)
|
||||
|
||||
result.set(timestamp, {
|
||||
timestamp,
|
||||
value: Math.round(cpuUsageSum / cpus.length),
|
||||
})
|
||||
}
|
||||
return [
|
||||
{
|
||||
label: t('stacked-cpu-usage'),
|
||||
data: Array.from(result.values()),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const maxValue = computed(() => {
|
||||
const values = cpuUsage.value[0]?.data.map(item => item.value) ?? []
|
||||
|
||||
if (values.length === 0) {
|
||||
return 100
|
||||
}
|
||||
|
||||
const maxCpuUsage = Math.max(...values)
|
||||
|
||||
return Math.ceil(maxCpuUsage / 100) * 100
|
||||
})
|
||||
|
||||
const valueFormatter: ValueFormatter = value => n(value / 100, 'percent')
|
||||
</script>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<UiCard>
|
||||
<UiCardTitle>
|
||||
{{ t('network-throughput') }}
|
||||
<template #description>{{ t('last-week') }}</template>
|
||||
</UiCardTitle>
|
||||
<VtsLoadingHero v-if="loading" type="card" />
|
||||
<VtsErrorNoDataHero v-else-if="error" type="card" />
|
||||
<VtsNoDataHero v-else-if="networkUsage.length === 0" type="card" />
|
||||
<VtsLinearChart v-else :data="networkUsage" :max-value :value-formatter="byteFormatter" />
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { XoVmStats } from '@/types/xo/vm-stats.type.ts'
|
||||
import type { LinearChartData } from '@core/types/chart.ts'
|
||||
import VtsErrorNoDataHero from '@core/components/state-hero/VtsErrorNoDataHero.vue'
|
||||
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
|
||||
import VtsNoDataHero from '@core/components/state-hero/VtsNoDataHero.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
|
||||
import { formatSizeRaw } from '@core/utils/size.util.ts'
|
||||
import { computed, defineAsyncComponent } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { data } = defineProps<{
|
||||
data: XoVmStats | null
|
||||
loading: boolean
|
||||
error?: string
|
||||
}>()
|
||||
|
||||
const VtsLinearChart = defineAsyncComponent(() => import('@core/components/linear-chart/VtsLinearChart.vue'))
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const networkUsage = computed<LinearChartData>(() => {
|
||||
if (!data?.stats.vifs) {
|
||||
return []
|
||||
}
|
||||
|
||||
const rxArrays = Object.values(data.stats.vifs.rx)
|
||||
|
||||
const timestamps = Array.from(
|
||||
{ length: rxArrays[0].length },
|
||||
(_, i) => data.endTimestamp * 1000 - (rxArrays[0].length - 1 - i) * data.interval * 1000
|
||||
)
|
||||
|
||||
const rxSeries = [
|
||||
{
|
||||
label: t('network-upload'),
|
||||
data: timestamps.map((timestamp, index) => ({
|
||||
timestamp,
|
||||
value: Object.values(data.stats.vifs.rx).reduce((sum, values) => sum + values[index], 0),
|
||||
})),
|
||||
},
|
||||
]
|
||||
|
||||
const txSeries = [
|
||||
{
|
||||
label: t('network-download'),
|
||||
data: timestamps.map((timestamp, index) => ({
|
||||
timestamp,
|
||||
value: Object.values(data.stats.vifs.tx).reduce((sum, values) => sum + values[index], 0),
|
||||
})),
|
||||
},
|
||||
]
|
||||
|
||||
return [...rxSeries, ...txSeries]
|
||||
})
|
||||
|
||||
const maxValue = computed(() => {
|
||||
const values = networkUsage.value.flatMap(series => series.data.map(item => item.value))
|
||||
|
||||
if (values.length === 0) {
|
||||
return 100
|
||||
}
|
||||
|
||||
const maxUsage = Math.max(...values)
|
||||
|
||||
return Math.ceil(maxUsage / 50) * 50
|
||||
})
|
||||
|
||||
const byteFormatter = (value: number) => {
|
||||
const result = formatSizeRaw(value, 1)
|
||||
|
||||
return `${result?.value}${result?.prefix}`
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<VtsQuickInfoCard class="vm-dashboard-quick-info" :loading="!isReady">
|
||||
<VtsQuickInfoColumn>
|
||||
<VtsQuickInfoRow :label="t('state')">
|
||||
<template #value>
|
||||
<span class="power-state">
|
||||
<VtsIcon :accent="powerState.accent" :icon="powerState.icon" />
|
||||
{{ powerState.text }}
|
||||
</span>
|
||||
</template>
|
||||
</VtsQuickInfoRow>
|
||||
<VtsQuickInfoRow :label="t('ip-address')" :value="vm.mainIpAddress" />
|
||||
<VtsQuickInfoRow :label="t('created-on')" :value="installDateFormatted" />
|
||||
<VtsQuickInfoRow :label="t('started')" :value="relativeStartTime" />
|
||||
<VtsQuickInfoRow :label="t('host')">
|
||||
<template #value>
|
||||
<span v-if="host" class="host-name">
|
||||
<UiLink :icon="faServer" :to="`/host/${host.id}`" size="medium">
|
||||
{{ host.name_label }}
|
||||
</UiLink>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('none') }}
|
||||
</span>
|
||||
</template>
|
||||
</VtsQuickInfoRow>
|
||||
</VtsQuickInfoColumn>
|
||||
<VtsQuickInfoColumn>
|
||||
<VtsQuickInfoRow :label="t('uuid')" :value="vm.id" />
|
||||
<VtsQuickInfoRow :label="t('description')" :value="vm.name_description" />
|
||||
<VtsQuickInfoRow :label="t('os-name')" :value="vm.os_version?.name" />
|
||||
<VtsQuickInfoRow :label="t('virtualization-type')" :value="virtualizationType" />
|
||||
</VtsQuickInfoColumn>
|
||||
<VtsQuickInfoColumn>
|
||||
<VtsQuickInfoRow :label="t('vcpus')" :value="String(vm.CPUs.number)" />
|
||||
<VtsQuickInfoRow :label="t('ram')" :value="`${ram?.value} ${ram?.prefix}`" />
|
||||
<VtsQuickInfoRow :label="t('tags')">
|
||||
<template #value>
|
||||
<UiTagsList v-if="vm.tags.length > 0">
|
||||
<UiTag v-for="tag in vm.tags" :key="tag" accent="info" variant="secondary">
|
||||
{{ tag }}
|
||||
</UiTag>
|
||||
</UiTagsList>
|
||||
</template>
|
||||
</VtsQuickInfoRow>
|
||||
</VtsQuickInfoColumn>
|
||||
</VtsQuickInfoCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useVmStore } from '@/stores/xo-rest-api/vm.store.ts'
|
||||
import { VM_POWER_STATE, type XoVm } from '@/types/xo/vm.type.ts'
|
||||
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
||||
import VtsQuickInfoCard from '@core/components/quick-info-card/VtsQuickInfoCard.vue'
|
||||
import VtsQuickInfoColumn from '@core/components/quick-info-column/VtsQuickInfoColumn.vue'
|
||||
import VtsQuickInfoRow from '@core/components/quick-info-row/VtsQuickInfoRow.vue'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import UiTag from '@core/components/ui/tag/UiTag.vue'
|
||||
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
|
||||
import { useTimeAgo } from '@core/composables/local-time-ago.composable.ts'
|
||||
import { useMapper } from '@core/packages/mapper'
|
||||
import { formatSizeRaw } from '@core/utils/size.util.ts'
|
||||
import { parseDateTime } from '@core/utils/time.util.ts'
|
||||
import { faMoon, faPause, faPlay, faServer, faStop } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { vm } = defineProps<{
|
||||
vm: XoVm
|
||||
}>()
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const { isReady, getVmHost } = useVmStore().subscribe()
|
||||
|
||||
const host = computed(() => getVmHost(vm))
|
||||
|
||||
// TODO add this to icon when new component is available
|
||||
// const hostPowerState = computed(() => {
|
||||
// if (host.value === undefined) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// return host.value.power_state === 'Running' ? 'running' : 'halted'
|
||||
// })
|
||||
|
||||
// TODO as above, add this to icon when new component is available
|
||||
const powerState = useMapper(
|
||||
() => vm.power_state,
|
||||
{
|
||||
[VM_POWER_STATE.RUNNING]: { icon: faPlay, accent: 'success', text: t('vm-status.running') },
|
||||
[VM_POWER_STATE.HALTED]: { icon: faStop, accent: 'danger', text: t('vm-status.halted') },
|
||||
[VM_POWER_STATE.PAUSED]: { icon: faPause, accent: 'brand', text: t('vm-status.paused') },
|
||||
[VM_POWER_STATE.SUSPENDED]: { icon: faMoon, accent: 'info', text: t('vm-status.suspended') },
|
||||
},
|
||||
VM_POWER_STATE.RUNNING
|
||||
)
|
||||
|
||||
const ram = computed(() => formatSizeRaw(vm.memory.size, 1))
|
||||
|
||||
const vmTimeAgo = useTimeAgo(() => new Date(parseDateTime((vm.startTime ?? 0) * 1000)))
|
||||
|
||||
const relativeStartTime = computed(() => {
|
||||
if (!vm.startTime || vm.power_state === VM_POWER_STATE.HALTED) {
|
||||
return t('not-running')
|
||||
}
|
||||
|
||||
return vmTimeAgo.value
|
||||
})
|
||||
|
||||
const installDateFormatted = computed(() => {
|
||||
if (!vm.installTime) {
|
||||
return t('unknown')
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat(locale.value, { dateStyle: 'long' }).format(
|
||||
new Date(parseDateTime(vm.installTime * 1000))
|
||||
)
|
||||
})
|
||||
|
||||
const virtualizationType = computed(() =>
|
||||
vm.virtualizationMode === 'hvm' && vm.pvDriversDetected ? 'pvhvm' : vm.virtualizationMode
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.vm-dashboard-quick-info {
|
||||
.power-state,
|
||||
.host-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<UiCard>
|
||||
<UiCardTitle>
|
||||
{{ t('ram-usage') }}
|
||||
<template #description>{{ t('last-week') }}</template>
|
||||
</UiCardTitle>
|
||||
<VtsLoadingHero v-if="loading" type="card" />
|
||||
<VtsErrorNoDataHero v-else-if="error" type="card" />
|
||||
<VtsNoDataHero v-else-if="ramUsage.length === 0" type="card" />
|
||||
<VtsLinearChart v-else :data="ramUsage" :max-value :value-formatter="byteFormatter" />
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { XoVmStats } from '@/types/xo/vm-stats.type.ts'
|
||||
import type { LinearChartData } from '@core/types/chart.ts'
|
||||
import VtsErrorNoDataHero from '@core/components/state-hero/VtsErrorNoDataHero.vue'
|
||||
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
|
||||
import VtsNoDataHero from '@core/components/state-hero/VtsNoDataHero.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
|
||||
import { formatSizeRaw } from '@core/utils/size.util.ts'
|
||||
import { computed, defineAsyncComponent } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { data } = defineProps<{
|
||||
data: XoVmStats | null
|
||||
loading: boolean
|
||||
error?: string
|
||||
}>()
|
||||
|
||||
const VtsLinearChart = defineAsyncComponent(() => import('@core/components/linear-chart/VtsLinearChart.vue'))
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const ramUsage = computed<LinearChartData>(() => {
|
||||
if (!data?.stats.memory || !data?.stats.memoryFree) {
|
||||
return []
|
||||
}
|
||||
|
||||
const ramTotalValues = data.stats.memory
|
||||
|
||||
const ramFreeValues = data.stats.memoryFree
|
||||
|
||||
const result = new Map<number, { timestamp: number; value: number }>()
|
||||
|
||||
const timestampStart = data.endTimestamp - data.interval * (ramTotalValues.length - 1)
|
||||
|
||||
for (let hourIndex = 0; hourIndex < ramTotalValues.length; hourIndex++) {
|
||||
const timestamp = (timestampStart + hourIndex * data.interval) * 1000
|
||||
|
||||
const ramTotal = ramTotalValues[hourIndex]
|
||||
|
||||
const ramFree = ramFreeValues[hourIndex]
|
||||
|
||||
const ramUsed = ramTotal - ramFree
|
||||
|
||||
result.set(timestamp, {
|
||||
timestamp,
|
||||
value: ramUsed,
|
||||
})
|
||||
}
|
||||
return [
|
||||
{
|
||||
label: t('stacked-ram-usage'),
|
||||
data: Array.from(result.values()),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const maxValue = computed(() => {
|
||||
if (!data?.stats.memory?.length) {
|
||||
return 1024 * 1024 * 1024 // 1 GB as fallback
|
||||
}
|
||||
|
||||
return Math.max(...data.stats.memory, 0)
|
||||
})
|
||||
|
||||
const byteFormatter = (value: number) => {
|
||||
const result = formatSizeRaw(value, 1)
|
||||
|
||||
return `${result?.value}${result?.prefix}`
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<UiCard>
|
||||
<UiCardTitle>
|
||||
{{ t('vdi-throughput') }}
|
||||
<template #description>{{ t('last-week') }}</template>
|
||||
</UiCardTitle>
|
||||
<VtsLoadingHero v-if="loading" type="card" />
|
||||
<VtsErrorNoDataHero v-else-if="error" type="card" />
|
||||
<VtsNoDataHero v-else-if="vdiUsage.length === 0" type="card" />
|
||||
<VtsLinearChart v-else :data="vdiUsage" :max-value :value-formatter="byteFormatter" />
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { XoVmStats } from '@/types/xo/vm-stats.type.ts'
|
||||
import type { LinearChartData } from '@core/types/chart.ts'
|
||||
import VtsErrorNoDataHero from '@core/components/state-hero/VtsErrorNoDataHero.vue'
|
||||
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
|
||||
import VtsNoDataHero from '@core/components/state-hero/VtsNoDataHero.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
|
||||
import { formatSizeRaw } from '@core/utils/size.util.ts'
|
||||
import { computed, defineAsyncComponent } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { data } = defineProps<{
|
||||
data: XoVmStats | null
|
||||
loading: boolean
|
||||
error?: string
|
||||
}>()
|
||||
|
||||
const VtsLinearChart = defineAsyncComponent(() => import('@core/components/linear-chart/VtsLinearChart.vue'))
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const vdiUsage = computed<LinearChartData>(() => {
|
||||
if (!data?.stats.xvds) {
|
||||
return []
|
||||
}
|
||||
|
||||
const readArrays = Object.values(data.stats.xvds.r)
|
||||
|
||||
const timestamps = Array.from(
|
||||
{ length: readArrays[0].length },
|
||||
(_, i) => data.endTimestamp * 1000 - (readArrays[0].length - 1 - i) * data.interval * 1000
|
||||
)
|
||||
|
||||
const readSeries = [
|
||||
{
|
||||
label: t('read'),
|
||||
data: timestamps.map((timestamp, index) => ({
|
||||
timestamp,
|
||||
value: Object.values(data.stats.xvds.r).reduce((sum, values) => sum + values[index], 0),
|
||||
})),
|
||||
},
|
||||
]
|
||||
|
||||
const writeSeries = [
|
||||
{
|
||||
label: t('write'),
|
||||
data: timestamps.map((timestamp, index) => ({
|
||||
timestamp,
|
||||
value: Object.values(data.stats.xvds.w).reduce((sum, values) => sum + values[index], 0),
|
||||
})),
|
||||
},
|
||||
]
|
||||
|
||||
return [...readSeries, ...writeSeries]
|
||||
})
|
||||
|
||||
const maxValue = computed(() => {
|
||||
const values = vdiUsage.value.flatMap(series => series.data.map(item => item.value))
|
||||
|
||||
if (values.length === 0) {
|
||||
return 100
|
||||
}
|
||||
|
||||
const maxUsage = Math.max(...values)
|
||||
|
||||
return Math.ceil(maxUsage / 50) * 50
|
||||
})
|
||||
|
||||
const byteFormatter = (value: number) => {
|
||||
const result = formatSizeRaw(value, 1)
|
||||
return `${result?.value}${result?.prefix}`
|
||||
}
|
||||
</script>
|
||||
@@ -166,8 +166,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useNetworkStore } from '@/stores/xo-rest-api/network.store'
|
||||
import { useVmStore } from '@/stores/xo-rest-api/vm.store.ts'
|
||||
import type { XoVif } from '@/types/xo/vif.type.ts'
|
||||
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
|
||||
import type { XoVif } from '@/types/xo/vif.type'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsConnectionStatus from '@core/components/connection-status/VtsConnectionStatus.vue'
|
||||
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
|
||||
@@ -130,9 +130,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useNetworkStore } from '@/stores/xo-rest-api/network.store'
|
||||
import { useVifStore } from '@/stores/xo-rest-api/vif.store.ts'
|
||||
import { useVmStore } from '@/stores/xo-rest-api/vm.store.ts'
|
||||
import type { XoVif } from '@/types/xo/vif.type.ts'
|
||||
import { useVifStore } from '@/stores/xo-rest-api/vif.store'
|
||||
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
|
||||
import type { XoVif } from '@/types/xo/vif.type'
|
||||
import VtsConnectionStatus from '@core/components/connection-status/VtsConnectionStatus.vue'
|
||||
import VtsDataTable from '@core/components/data-table/VtsDataTable.vue'
|
||||
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
||||
@@ -1,11 +1,17 @@
|
||||
import type { XoHost } from '@/types/xo/host.type.ts'
|
||||
import { GRANULARITY, RRD_STEP_FROM_STRING } from '@/utils/rest-api-stats.ts'
|
||||
import type { XoPool } from '@/types/xo/pool.type.ts'
|
||||
import type { XoVm } from '@/types/xo/vm.type.ts'
|
||||
import { type GRANULARITY, RRD_STEP_FROM_STRING } from '@/utils/rest-api-stats.ts'
|
||||
import { useFetch, useIntervalFn } from '@vueuse/core'
|
||||
import type { MaybeRefOrGetter } from '@vueuse/shared'
|
||||
import { toValue } from 'vue'
|
||||
|
||||
export function useFetchStats(hostId: MaybeRefOrGetter<XoHost['id']>, granularity: MaybeRefOrGetter<GRANULARITY>) {
|
||||
const result = useFetch(() => `/rest/v0/hosts/${toValue(hostId)}/stats?granularity=${toValue(granularity)}`, {
|
||||
export function useFetchStats(
|
||||
objectType: 'host' | 'vm' | 'pool',
|
||||
id: MaybeRefOrGetter<XoVm['id'] | XoHost['id'] | XoPool['id']>,
|
||||
granularity: MaybeRefOrGetter<GRANULARITY>
|
||||
) {
|
||||
const result = useFetch(() => `/rest/v0/${objectType}s/${toValue(id)}/stats?granularity=${toValue(granularity)}`, {
|
||||
immediate: false,
|
||||
refetch: true,
|
||||
beforeFetch({ options }) {
|
||||
@@ -17,9 +23,11 @@ export function useFetchStats(hostId: MaybeRefOrGetter<XoHost['id']>, granularit
|
||||
.get()
|
||||
.json()
|
||||
|
||||
useIntervalFn(() => result.execute(), RRD_STEP_FROM_STRING[toValue(granularity)] * 1000, {
|
||||
immediateCallback: true,
|
||||
})
|
||||
useIntervalFn(
|
||||
() => result.execute(),
|
||||
() => RRD_STEP_FROM_STRING[toValue(granularity)] * 1000,
|
||||
{ immediateCallback: true }
|
||||
)
|
||||
|
||||
return {
|
||||
data: result.data,
|
||||
|
||||
@@ -29,7 +29,7 @@ const { host } = defineProps<{
|
||||
host: XoHost
|
||||
}>()
|
||||
|
||||
const { data, isFetching, error } = useFetchStats(() => host.id, GRANULARITY.Hours)
|
||||
const { data, isFetching, error } = useFetchStats('host', () => host.id, GRANULARITY.Hours)
|
||||
|
||||
const uiStore = useUiStore()
|
||||
</script>
|
||||
|
||||
85
@xen-orchestra/web/src/pages/vm/[id]/dashboard.vue
Normal file
85
@xen-orchestra/web/src/pages/vm/[id]/dashboard.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="dashboard" :class="{ mobile: uiStore.isMobile }">
|
||||
<VmDashboardQuickInfo class="quick-info" :vm />
|
||||
<div v-if="!data" class="offline-hero-container">
|
||||
<VtsOfflineHero type="page" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<VmDashboardCpuUsageChart class="cpu-usage-chart" :data :error :loading="isFetching" />
|
||||
<VmDashboardRamUsageChart class="ram-usage-chart" :data :error :loading="isFetching" />
|
||||
<VmDashboardNetworkUsageChart class="network-usage-chart" :data :error :loading="isFetching" />
|
||||
<VmDashboardVdiUsageChart class="vdi-usage-chart" :data :error :loading="isFetching" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import VmDashboardCpuUsageChart from '@/components/vm/dashboard/VmDashboardCpuUsageChart.vue'
|
||||
import VmDashboardNetworkUsageChart from '@/components/vm/dashboard/VmDashboardNetworkUsageChart.vue'
|
||||
import VmDashboardQuickInfo from '@/components/vm/dashboard/VmDashboardQuickInfo.vue'
|
||||
import VmDashboardRamUsageChart from '@/components/vm/dashboard/VmDashboardRamUsageChart.vue'
|
||||
import VmDashboardVdiUsageChart from '@/components/vm/dashboard/VmDashboardVdiUsageChart.vue'
|
||||
import { useFetchStats } from '@/composables/fetch-stats.composable.ts'
|
||||
import { type XoVm } from '@/types/xo/vm.type'
|
||||
import { GRANULARITY } from '@/utils/rest-api-stats.ts'
|
||||
import VtsOfflineHero from '@core/components/state-hero/VtsOfflineHero.vue'
|
||||
import { useUiStore } from '@core/stores/ui.store.ts'
|
||||
|
||||
const { vm } = defineProps<{
|
||||
vm: XoVm
|
||||
}>()
|
||||
|
||||
const { data, isFetching, error } = useFetchStats('vm', () => vm.id, GRANULARITY.Hours)
|
||||
|
||||
const uiStore = useUiStore()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.dashboard {
|
||||
display: grid;
|
||||
margin: 0.8rem;
|
||||
gap: 0.8rem;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
grid-template-areas:
|
||||
'quick-info quick-info quick-info quick-info quick-info quick-info quick-info quick-info'
|
||||
'cpu-usage-chart cpu-usage-chart ram-usage-chart ram-usage-chart network-usage-chart network-usage-chart vdi-usage-chart vdi-usage-chart'
|
||||
'offline-hero-container offline-hero-container offline-hero-container offline-hero-container offline-hero-container offline-hero-container offline-hero-container offline-hero-container';
|
||||
|
||||
&.mobile {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
'quick-info'
|
||||
'cpu-usage-chart'
|
||||
'ram-usage-chart'
|
||||
'network-usage-chart'
|
||||
'vdi-usage-chart'
|
||||
'offline-hero-container';
|
||||
}
|
||||
|
||||
.quick-info {
|
||||
grid-area: quick-info;
|
||||
}
|
||||
|
||||
.offline-hero-container {
|
||||
grid-area: offline-hero-container;
|
||||
width: 50rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cpu-usage-chart {
|
||||
grid-area: cpu-usage-chart;
|
||||
}
|
||||
|
||||
.ram-usage-chart {
|
||||
grid-area: ram-usage-chart;
|
||||
}
|
||||
|
||||
.network-usage-chart {
|
||||
grid-area: network-usage-chart;
|
||||
}
|
||||
|
||||
.vdi-usage-chart {
|
||||
grid-area: vdi-usage-chart;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,6 @@
|
||||
// The `definePage` macro is removed during the build, leaving an empty script tag that causes the build to fail.
|
||||
definePage({
|
||||
name: '/vm/:id',
|
||||
redirect: (to: any) => `/vm/${to.params.id}/console`,
|
||||
redirect: (to: any) => `/vm/${to.params.id}/dashboard`,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import VmVifSidePanel from '@/components/vm/VmVifSidePanel.vue'
|
||||
import VmVifsTable from '@/components/vm/VmVifsTable.vue'
|
||||
import VmVifSidePanel from '@/components/vm/network/VmVifSidePanel.vue'
|
||||
import VmVifsTable from '@/components/vm/network/VmVifsTable.vue'
|
||||
import { useVifStore } from '@/stores/xo-rest-api/vif.store.ts'
|
||||
import type { XoVif } from '@/types/xo/vif.type.ts'
|
||||
import VtsNoSelectionHero from '@core/components/state-hero/VtsNoSelectionHero.vue'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useHostStore } from '@/stores/xo-rest-api/host.store.ts'
|
||||
import type { XoHost } from '@/types/xo/host.type'
|
||||
import type { XoPool } from '@/types/xo/pool.type'
|
||||
import type { XoVm } from '@/types/xo/vm.type'
|
||||
@@ -10,6 +11,12 @@ import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
export const useVmStore = defineStore('vm', () => {
|
||||
const deps = {
|
||||
hostStore: useHostStore(),
|
||||
}
|
||||
|
||||
const hostContext = deps.hostStore.getContext()
|
||||
|
||||
const { context: baseContext, ...configRest } = createXoStoreConfig('vm', {
|
||||
sortBy: sortByNameLabel,
|
||||
})
|
||||
@@ -26,10 +33,21 @@ export const useVmStore = defineStore('vm', () => {
|
||||
return castArray(operations).some(operation => currentOperations.includes(operation))
|
||||
}
|
||||
|
||||
const getVmHost = (vm: XoVm): XoHost | undefined => {
|
||||
const vmHostId = extractVmHostId(vm)
|
||||
|
||||
if (vmHostId === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return hostContext.get(vmHostId)
|
||||
}
|
||||
|
||||
const context = {
|
||||
...baseContext,
|
||||
runningVms,
|
||||
vmsByHost,
|
||||
getVmHost,
|
||||
hostLessVmsByPool,
|
||||
isVmOperatingPending,
|
||||
}
|
||||
@@ -58,3 +76,7 @@ function createVmsMap<THostLess extends boolean>(vms: XoVm[], hostLess: THostLes
|
||||
|
||||
return vmsMap
|
||||
}
|
||||
|
||||
function extractVmHostId(vm: XoVm) {
|
||||
return vm.$container === vm.$pool ? undefined : (vm.$container as XoHost['id'])
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type XoStats = Record<string, number[]>
|
||||
import type { XoStats } from '@/types/xo/xo-stats.type.ts'
|
||||
|
||||
export type XoHostStats = {
|
||||
endTimestamp: number
|
||||
|
||||
23
@xen-orchestra/web/src/types/xo/vm-stats.type.ts
Normal file
23
@xen-orchestra/web/src/types/xo/vm-stats.type.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { XoStats } from '@/types/xo/xo-stats.type.ts'
|
||||
|
||||
export type XoVmStats = {
|
||||
endTimestamp: number
|
||||
interval: number
|
||||
stats: {
|
||||
cpus: XoStats
|
||||
memory: number[]
|
||||
iops: {
|
||||
r: XoStats
|
||||
w: XoStats
|
||||
}
|
||||
xvds: {
|
||||
r: XoStats
|
||||
w: XoStats
|
||||
}
|
||||
vifs: {
|
||||
rx: XoStats
|
||||
tx: XoStats
|
||||
}
|
||||
memoryFree: number[]
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export type XoVm = {
|
||||
name_description: string
|
||||
power_state: VM_POWER_STATE
|
||||
addresses: Record<string, string>
|
||||
mainIpAddress: string
|
||||
mainIpAddress?: string
|
||||
other: { disable_pv_vnc: string }
|
||||
CPUs: {
|
||||
max: number
|
||||
@@ -55,6 +55,9 @@ export type XoVm = {
|
||||
high_availability: 'best-effort' | 'restart' | ''
|
||||
auto_poweron: boolean
|
||||
startDelay: number
|
||||
startTime?: number | null
|
||||
installTime?: number | null
|
||||
pvDriversDetected?: boolean
|
||||
vga: 'std' | 'cirrus'
|
||||
videoram?: number
|
||||
pvDriversVersion?: string
|
||||
|
||||
1
@xen-orchestra/web/src/types/xo/xo-stats.type.ts
Normal file
1
@xen-orchestra/web/src/types/xo/xo-stats.type.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type XoStats = Record<string, number[]>
|
||||
@@ -33,7 +33,7 @@ export const xoApiDefinition = {
|
||||
type: 'collection',
|
||||
path: 'vms',
|
||||
fields:
|
||||
'id,name_label,name_description,power_state,$container,$pool,other,current_operations,CPUs,addresses,tags,os_version,virtualizationMode,secureBoot,VTPMs,viridian,isNestedVirtEnabled,memory,VGPUs,high_availability,auto_poweron,startDelay,vga,videoram,pvDriversVersion,cpuWeight,cpuCap,cpuMask,coresPerSocket,nicType,affinityHost,suspendSr,blockedOperations,hasVendorDevice',
|
||||
'id,name_label,name_description,power_state,$container,$pool,other,current_operations,CPUs,addresses,tags,os_version,virtualizationMode,secureBoot,VTPMs,viridian,isNestedVirtEnabled,memory,VGPUs,high_availability,auto_poweron,startDelay,vga,videoram,pvDriversVersion,cpuWeight,cpuCap,cpuMask,coresPerSocket,mainIpAddress,nicType,affinityHost,suspendSr,blockedOperations,hasVendorDevice,startTime,installTime,pvDriversDetected',
|
||||
handler: (record: XoVm) => record,
|
||||
},
|
||||
sr: {
|
||||
|
||||
1
@xen-orchestra/web/typed-router.d.ts
vendored
1
@xen-orchestra/web/typed-router.d.ts
vendored
@@ -39,6 +39,7 @@ declare module 'vue-router/auto-routes' {
|
||||
'/vm/[id]': RouteRecordInfo<'/vm/[id]', '/vm/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'/vm/:id': RouteRecordInfo<'/vm/:id', '/vm/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'/vm/[id]/console': RouteRecordInfo<'/vm/[id]/console', '/vm/:id/console', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'/vm/[id]/dashboard': RouteRecordInfo<'/vm/[id]/dashboard', '/vm/:id/dashboard', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'/vm/[id]/networks': RouteRecordInfo<'/vm/[id]/networks', '/vm/:id/networks', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'/vm/[id]/system': RouteRecordInfo<'/vm/[id]/system', '/vm/:id/system', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'/vm/new': RouteRecordInfo<'/vm/new', '/vm/new', Record<never, never>, Record<never, never>>,
|
||||
|
||||
Reference in New Issue
Block a user