diff --git a/@xen-orchestra/lite/src/components/UsageBar.vue b/@xen-orchestra/lite/src/components/UsageBar.vue index b7332b3c7..0e44bdbcd 100644 --- a/@xen-orchestra/lite/src/components/UsageBar.vue +++ b/@xen-orchestra/lite/src/components/UsageBar.vue @@ -1,16 +1,18 @@ @@ -19,6 +21,7 @@ import { computed } from "vue"; import ProgressBar from "@/components/ProgressBar.vue"; interface Data { + id: string; value: number; label?: string; badgeLabel?: string; @@ -27,7 +30,7 @@ interface Data { interface Props { data: Array; - title?: string; + nItems?: number; } const props = defineProps(); @@ -45,7 +48,8 @@ const computedData = computed(() => { value, }; }) - .sort((item, nextItem) => nextItem.value - item.value), + .sort((item, nextItem) => nextItem.value - item.value) + .slice(0, props.nItems ?? _data.length), totalPercentUsage, }; }); diff --git a/@xen-orchestra/lite/src/components/pool/dashboard/PoolDashboardCpuUsage.vue b/@xen-orchestra/lite/src/components/pool/dashboard/PoolDashboardCpuUsage.vue new file mode 100644 index 000000000..4c3a40b81 --- /dev/null +++ b/@xen-orchestra/lite/src/components/pool/dashboard/PoolDashboardCpuUsage.vue @@ -0,0 +1,13 @@ + + diff --git a/@xen-orchestra/lite/src/components/pool/dashboard/cpuUsage/HostsCpuUsage.vue b/@xen-orchestra/lite/src/components/pool/dashboard/cpuUsage/HostsCpuUsage.vue new file mode 100644 index 000000000..f2dba995c --- /dev/null +++ b/@xen-orchestra/lite/src/components/pool/dashboard/cpuUsage/HostsCpuUsage.vue @@ -0,0 +1,46 @@ + + + diff --git a/@xen-orchestra/lite/src/components/pool/dashboard/cpuUsage/VmsCpuUsage.vue b/@xen-orchestra/lite/src/components/pool/dashboard/cpuUsage/VmsCpuUsage.vue new file mode 100644 index 000000000..fc7c6c20c --- /dev/null +++ b/@xen-orchestra/lite/src/components/pool/dashboard/cpuUsage/VmsCpuUsage.vue @@ -0,0 +1,46 @@ + + diff --git a/@xen-orchestra/lite/src/composables/fetch-stats.composable.md b/@xen-orchestra/lite/src/composables/fetch-stats.composable.md index 36d198fec..f6f16b214 100644 --- a/@xen-orchestra/lite/src/composables/fetch-stats.composable.md +++ b/@xen-orchestra/lite/src/composables/fetch-stats.composable.md @@ -1,23 +1,25 @@ # useFetchStats composable ```vue +
+

+ {{ stat.name }} +

+
``` diff --git a/@xen-orchestra/lite/src/composables/fetch-stats.composable.ts b/@xen-orchestra/lite/src/composables/fetch-stats.composable.ts index 81c99b765..2ed477a9f 100644 --- a/@xen-orchestra/lite/src/composables/fetch-stats.composable.ts +++ b/@xen-orchestra/lite/src/composables/fetch-stats.composable.ts @@ -1,6 +1,7 @@ -import { type Ref, ref } from "vue"; -import { promiseTimeout, useIntervalFn } from "@vueuse/core"; +import { computed, onUnmounted, ref } from "vue"; +import { type Pausable, promiseTimeout, useTimeoutPoll } from "@vueuse/core"; import type { GRANULARITY, XapiStatsResponse } from "@/libs/xapi-stats"; +import type { XenApiHost, XenApiVm } from "@/libs/xen-api"; import { useHostStore } from "@/stores/host.store"; import { useVmStore } from "@/stores/vm.store"; @@ -9,19 +10,59 @@ const STORES_BY_OBJECT_TYPE = { vm: useVmStore, }; -export default function useFetchStats( +export default function useFetchStats( type: "host" | "vm", - id: string, granularity: GRANULARITY ) { - const stats = ref(); - const fetch = STORES_BY_OBJECT_TYPE[type]().getStats; + const stats = ref< + Map + >(new Map()); - const fetchStats = async () => { - stats.value = await fetch(id, granularity); - await promiseTimeout(stats.value.interval * 1000); + const register = (object: T) => { + if (stats.value.has(object.uuid)) { + stats.value.get(object.uuid)!.pausable.resume(); + return; + } + + const pausable = useTimeoutPoll( + async () => { + if (!stats.value.has(object.uuid)) { + return; + } + + const newStats = (await STORES_BY_OBJECT_TYPE[type]().getStats( + object.uuid, + granularity + )) as XapiStatsResponse; + + stats.value.get(object.uuid)!.stats = newStats.stats; + + await promiseTimeout(newStats.interval * 1000); + }, + 0, + { immediate: true } + ); + + stats.value.set(object.uuid, { + id: object.uuid, + name: object.name_label, + stats: undefined, + pausable, + }); }; - useIntervalFn(fetchStats); - return { stats } as { stats: Ref> | undefined }; + const unregister = (object: T) => { + stats.value.get(object.uuid)?.pausable.pause(); + stats.value.delete(object.uuid); + }; + + onUnmounted(() => { + stats.value.forEach((stat) => stat.pausable.pause()); + }); + + return { + register, + unregister, + stats: computed(() => Array.from(stats.value.values())), + }; } diff --git a/@xen-orchestra/lite/src/libs/utils.ts b/@xen-orchestra/lite/src/libs/utils.ts index 1c384aa94..31fbf13fd 100644 --- a/@xen-orchestra/lite/src/libs/utils.ts +++ b/@xen-orchestra/lite/src/libs/utils.ts @@ -1,9 +1,13 @@ import { utcParse } from "d3-time-format"; import humanFormat from "human-format"; import { round } from "lodash-es"; +import { find, forEach, isEqual, size, sum } from "lodash-es"; +import { type ComputedGetter, type Ref, computed, ref, watchEffect } from "vue"; import type { Filter } from "@/types/filter"; import { faSquareCheck } from "@fortawesome/free-regular-svg-icons"; import { faFont, faHashtag, faList } from "@fortawesome/free-solid-svg-icons"; +import type { RawXenApiRecord, XenApiHost, XenApiRecord } from "@/libs/xen-api"; +import { useHostMetricsStore } from "@/stores/host-metrics.store"; export function sortRecordsByNameLabel( record1: { name_label: string }, @@ -67,3 +71,55 @@ export const hasEllipsis = (target: Element | undefined | null) => export function percent(currentValue: number, maxValue: number, precision = 2) { return round((currentValue / maxValue) * 100, precision); } +export function getAvgCpuUsage(cpus?: object | any[], { nSequence = 4 } = {}) { + const statsLength = getStatsLength(cpus); + if (statsLength === undefined) { + return; + } + const _nSequence = statsLength < nSequence ? statsLength : nSequence; + + let totalCpusUsage = 0; + forEach(cpus, (cpuState: number[]) => { + totalCpusUsage += sum(cpuState.slice(cpuState.length - _nSequence)); + }); + const stackedValue = totalCpusUsage / _nSequence; + return stackedValue / size(cpus); +} + +// stats can be null. +// Return the size of the first non-null object. +export function getStatsLength(stats?: object | any[]) { + if (stats === undefined) { + return undefined; + } + return size(find(stats, (stat) => stat != null)); +} + +export function deepComputed(getter: ComputedGetter) { + const value = computed(getter); + const cache = ref(value.value) as Ref; + watchEffect(() => { + if (!isEqual(cache.value, value.value)) { + cache.value = value.value; + } + }); + + return cache; +} + +export function isHostRunning(host: XenApiHost) { + const store = useHostMetricsStore(); + try { + return store.getRecord(host.metrics).live; + } catch (_) { + return undefined; + } +} + +export const buildXoObject = ( + record: RawXenApiRecord, + params: { opaqueRef: string } +) => ({ + ...record, + $ref: params.opaqueRef, +}); diff --git a/@xen-orchestra/lite/src/libs/xen-api.ts b/@xen-orchestra/lite/src/libs/xen-api.ts index bc5550b73..c68c79a98 100644 --- a/@xen-orchestra/lite/src/libs/xen-api.ts +++ b/@xen-orchestra/lite/src/libs/xen-api.ts @@ -1,5 +1,5 @@ import { JSONRPCClient } from "json-rpc-2.0"; -import { parseDateTime } from "@/libs/utils"; +import { buildXoObject, parseDateTime } from "@/libs/utils"; export type RawObjectType = | "Bond" @@ -66,7 +66,7 @@ export interface XenApiRecord { uuid: string; } -type RawXenApiRecord = Omit; +export type RawXenApiRecord = Omit; export interface XenApiPool extends XenApiRecord { name_label: string; @@ -232,7 +232,7 @@ export default class XenApi { const entries = Object.entries(result).map<[string, T]>(([key, entry]) => [ key, - { $ref: key, ...entry } as T, + buildXoObject(entry, { opaqueRef: key }) as T, ]); return new Map(entries); diff --git a/@xen-orchestra/lite/src/locales/en.json b/@xen-orchestra/lite/src/locales/en.json index 37c463fb2..2b7734d72 100644 --- a/@xen-orchestra/lite/src/locales/en.json +++ b/@xen-orchestra/lite/src/locales/en.json @@ -10,6 +10,7 @@ "cancel": "Cancel", "change-power-state": "Change power state", "copy": "Copy", + "cpu-usage":"CPU usage", "dashboard": "Dashboard", "delete": "Delete", "descending": "descending", diff --git a/@xen-orchestra/lite/src/locales/fr.json b/@xen-orchestra/lite/src/locales/fr.json index b7afc0009..4f4869ec1 100644 --- a/@xen-orchestra/lite/src/locales/fr.json +++ b/@xen-orchestra/lite/src/locales/fr.json @@ -10,6 +10,7 @@ "cancel": "Annuler", "change-power-state": "Changer l'état d'alimentation", "copy": "Copier", + "cpu-usage":"Utilisation CPU", "dashboard": "Tableau de bord", "delete": "Supprimer", "descending": "descendant", diff --git a/@xen-orchestra/lite/src/stores/records.store.ts b/@xen-orchestra/lite/src/stores/records.store.ts index 7e5afad2a..6fd6c15bb 100644 --- a/@xen-orchestra/lite/src/stores/records.store.ts +++ b/@xen-orchestra/lite/src/stores/records.store.ts @@ -1,5 +1,6 @@ import { defineStore } from "pinia"; import { reactive, shallowReactive } from "vue"; +import { buildXoObject } from "@/libs/utils"; import type { ObjectType, RawObjectType, XenApiRecord } from "@/libs/xen-api"; import { useXenApiStore } from "@/stores/xen-api.store"; @@ -38,7 +39,7 @@ export const useRecordsStore = defineStore("records", () => { opaqueRef: string, record: T ) { - recordsByOpaqueRef.set(opaqueRef, record); + recordsByOpaqueRef.set(opaqueRef, buildXoObject(record, { opaqueRef })); opaqueRefsByObjectType.get(objectType)?.add(opaqueRef); uuidToOpaqueRefMapping.set(record.uuid, opaqueRef); } diff --git a/@xen-orchestra/lite/src/views/pool/PoolDashboardView.vue b/@xen-orchestra/lite/src/views/pool/PoolDashboardView.vue index 98c8ca08a..fd43d0551 100644 --- a/@xen-orchestra/lite/src/views/pool/PoolDashboardView.vue +++ b/@xen-orchestra/lite/src/views/pool/PoolDashboardView.vue @@ -2,12 +2,65 @@
+