feat(lite/pool/dashboard): top 5 CPU usage (#6370)
This commit is contained in:
parent
ffc3249b33
commit
9963568368
@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div v-if="data.length !== 0">
|
||||
<div class="header">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<ProgressBar
|
||||
v-for="(item, index) in computedData.sortedArray"
|
||||
:key="index"
|
||||
v-for="item in computedData.sortedArray"
|
||||
:key="item.id"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
:badge-label="item.badgeLabel"
|
||||
@ -12,6 +13,7 @@
|
||||
<div class="footer">
|
||||
<slot name="footer" :total-percent="computedData.totalPercentUsage" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -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<Data>;
|
||||
title?: string;
|
||||
nItems?: number;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
@ -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,
|
||||
};
|
||||
});
|
||||
|
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<UiCard>
|
||||
<UiTitle type="h4">{{ $t("cpu-usage") }}</UiTitle>
|
||||
<HostsCpuUsage />
|
||||
<VmsCpuUsage />
|
||||
</UiCard>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import HostsCpuUsage from "@/components/pool/dashboard/cpuUsage/HostsCpuUsage.vue";
|
||||
import VmsCpuUsage from "@/components/pool/dashboard/cpuUsage/VmsCpuUsage.vue";
|
||||
import UiCard from "@/components/ui/UiCard.vue";
|
||||
import UiTitle from "@/components/ui/UiTitle.vue";
|
||||
</script>
|
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<UsageBar :data="data" :n-items="5">
|
||||
<template #header>
|
||||
<span>{{ $t("hosts") }}</span>
|
||||
<span>{{ $t("top-#", { n: 5 }) }}</span>
|
||||
</template>
|
||||
</UsageBar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ComputedRef, computed, inject } from "vue";
|
||||
import UsageBar from "@/components/UsageBar.vue";
|
||||
import { getAvgCpuUsage } from "@/libs/utils";
|
||||
import type { HostStats } from "@/libs/xapi-stats";
|
||||
|
||||
const stats: ComputedRef<
|
||||
{
|
||||
id: string;
|
||||
name: string;
|
||||
stats?: HostStats;
|
||||
}[]
|
||||
> = inject<any>("hostStats", []);
|
||||
|
||||
const data = computed<{ id: string; label: string; value: number }[]>(() => {
|
||||
const result: { id: string; label: string; value: number }[] = [];
|
||||
|
||||
stats.value.forEach((stat) => {
|
||||
if (stat.stats === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const avgCpuUsage = getAvgCpuUsage(stat.stats.cpus);
|
||||
|
||||
if (avgCpuUsage === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.push({
|
||||
id: stat.id,
|
||||
label: stat.name,
|
||||
value: avgCpuUsage,
|
||||
});
|
||||
});
|
||||
return result;
|
||||
});
|
||||
</script>
|
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<UsageBar :data="data" :n-items="5">
|
||||
<template #header>
|
||||
<span>{{ $t("vms") }}</span>
|
||||
<span>{{ $t("top-#", { n: 5 }) }}</span>
|
||||
</template>
|
||||
</UsageBar>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { type ComputedRef, computed, inject } from "vue";
|
||||
import UsageBar from "@/components/UsageBar.vue";
|
||||
import { getAvgCpuUsage } from "@/libs/utils";
|
||||
import type { VmStats } from "@/libs/xapi-stats";
|
||||
|
||||
const stats: ComputedRef<
|
||||
{
|
||||
id: string;
|
||||
name: string;
|
||||
stats?: VmStats;
|
||||
}[]
|
||||
> = inject<any>("vmStats", []);
|
||||
|
||||
const data = computed<{ id: string; label: string; value: number }[]>(() => {
|
||||
const result: { id: string; label: string; value: number }[] = [];
|
||||
|
||||
stats.value.forEach((stat) => {
|
||||
if (!stat.stats) {
|
||||
return;
|
||||
}
|
||||
|
||||
const avgCpuUsage = getAvgCpuUsage(stat.stats.cpus);
|
||||
|
||||
if (avgCpuUsage === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.push({
|
||||
id: stat.id,
|
||||
label: stat.name,
|
||||
value: avgCpuUsage,
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
</script>
|
@ -1,23 +1,25 @@
|
||||
# useFetchStats composable
|
||||
|
||||
```vue
|
||||
<div>
|
||||
<p v-for="(stat, index) in stats" :key="index">
|
||||
{{ stat.name }}
|
||||
</p>
|
||||
</div>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from "vue";
|
||||
import useFetchStats from "@/composables/fetch-stats-composable";
|
||||
import { GRANULARITY, type HostStats, type VmStats } from "@/libs/xapi-stats";
|
||||
|
||||
const vmId = "1d381a66-d1cb-bb7e-50a1-feeab58b293d";
|
||||
const hostId = "0aea61f4-c9d1-4060-94e8-4eb2024d082c";
|
||||
const vmStore = useVmStore();
|
||||
|
||||
const { stats: vmStats } = useFetchStats<VmStats>(
|
||||
const { register, unregister, stats } = useFetchStats<XenApiVm, VmStats>(
|
||||
"vm",
|
||||
vmId,
|
||||
GRANULARITY.Seconds
|
||||
);
|
||||
|
||||
const { stats: hostStats } = useFetchStats<HostStats>(
|
||||
"host",
|
||||
hostId,
|
||||
GRANULARITY.Seconds
|
||||
);
|
||||
onMounted(() => {
|
||||
vmStore.allRecords.forEach(register);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
@ -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<T>(
|
||||
export default function useFetchStats<T extends XenApiHost | XenApiVm, S>(
|
||||
type: "host" | "vm",
|
||||
id: string,
|
||||
granularity: GRANULARITY
|
||||
) {
|
||||
const stats = ref();
|
||||
const fetch = STORES_BY_OBJECT_TYPE[type]().getStats;
|
||||
const stats = ref<
|
||||
Map<string, { id: string; name: string; stats?: S; pausable: Pausable }>
|
||||
>(new Map());
|
||||
|
||||
const fetchStats = async () => {
|
||||
stats.value = await fetch(id, granularity);
|
||||
await promiseTimeout(stats.value.interval * 1000);
|
||||
};
|
||||
useIntervalFn(fetchStats);
|
||||
|
||||
return { stats } as { stats: Ref<XapiStatsResponse<T>> | undefined };
|
||||
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<S>;
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
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())),
|
||||
};
|
||||
}
|
||||
|
@ -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<T>(getter: ComputedGetter<T>) {
|
||||
const value = computed(getter);
|
||||
const cache = ref<T>(value.value) as Ref<T>;
|
||||
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<XenApiRecord>,
|
||||
params: { opaqueRef: string }
|
||||
) => ({
|
||||
...record,
|
||||
$ref: params.opaqueRef,
|
||||
});
|
||||
|
@ -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<T extends XenApiRecord> = Omit<T, "$ref">;
|
||||
export type RawXenApiRecord<T extends XenApiRecord> = Omit<T, "$ref">;
|
||||
|
||||
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);
|
||||
|
@ -10,6 +10,7 @@
|
||||
"cancel": "Cancel",
|
||||
"change-power-state": "Change power state",
|
||||
"copy": "Copy",
|
||||
"cpu-usage":"CPU usage",
|
||||
"dashboard": "Dashboard",
|
||||
"delete": "Delete",
|
||||
"descending": "descending",
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -2,12 +2,65 @@
|
||||
<div class="pool-dashboard-view">
|
||||
<PoolDashboardStatus class="item" />
|
||||
<PoolDashboardStorageUsage class="item" />
|
||||
<PoolDashboardCpuUsage class="item" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { differenceBy } from "lodash-es";
|
||||
import { computed, onMounted, provide, readonly, watch } from "vue";
|
||||
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.vue";
|
||||
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
|
||||
import PoolDashboardStorageUsage from "@/components/pool/dashboard/PoolDashboardStorageUsage.vue";
|
||||
import useFetchStats from "@/composables/fetch-stats.composable";
|
||||
import { isHostRunning } from "@/libs/utils";
|
||||
import { GRANULARITY, type HostStats, type VmStats } from "@/libs/xapi-stats";
|
||||
import type { XenApiHost, XenApiVm } from "@/libs/xen-api";
|
||||
import { useHostStore } from "@/stores/host.store";
|
||||
import { useVmStore } from "@/stores/vm.store";
|
||||
|
||||
const hostStore = useHostStore();
|
||||
const vmStore = useVmStore();
|
||||
|
||||
const {
|
||||
register: hostRegister,
|
||||
unregister: hostUnregister,
|
||||
stats: hostStats,
|
||||
} = useFetchStats<XenApiHost, HostStats>("host", GRANULARITY.Seconds);
|
||||
const {
|
||||
register: vmRegister,
|
||||
unregister: vmUnregister,
|
||||
stats: vmStats,
|
||||
} = useFetchStats<XenApiVm, VmStats>("vm", GRANULARITY.Seconds);
|
||||
|
||||
const runningHosts = computed(() => hostStore.allRecords.filter(isHostRunning));
|
||||
const runningVms = computed(() =>
|
||||
vmStore.allRecords.filter((vm) => vm.power_state === "Running")
|
||||
);
|
||||
|
||||
provide("hostStats", readonly(hostStats));
|
||||
provide("vmStats", readonly(vmStats));
|
||||
|
||||
watch(runningHosts, (hosts, previousHosts) => {
|
||||
// turned On
|
||||
differenceBy(hosts, previousHosts ?? [], "uuid").forEach(hostRegister);
|
||||
|
||||
// turned Off
|
||||
differenceBy(previousHosts, hosts, "uuid").forEach(hostUnregister);
|
||||
});
|
||||
|
||||
watch(runningVms, (vms, previousVms) => {
|
||||
// turned On
|
||||
differenceBy(vms, previousVms ?? [], "uuid").forEach(vmRegister);
|
||||
|
||||
// turned Off
|
||||
differenceBy(previousVms, vms, "uuid").forEach(vmUnregister);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
runningHosts.value.forEach(hostRegister);
|
||||
runningVms.value.forEach(vmRegister);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
Loading…
Reference in New Issue
Block a user