feat(lite/pool/dashboard): top 5 CPU usage (#6370)
This commit is contained in:
parent
ffc3249b33
commit
9963568368
@ -1,16 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="header">
|
<div v-if="data.length !== 0">
|
||||||
<slot name="header" />
|
<div class="header">
|
||||||
</div>
|
<slot name="header" />
|
||||||
<ProgressBar
|
</div>
|
||||||
v-for="(item, index) in computedData.sortedArray"
|
<ProgressBar
|
||||||
:key="index"
|
v-for="item in computedData.sortedArray"
|
||||||
:value="item.value"
|
:key="item.id"
|
||||||
:label="item.label"
|
:value="item.value"
|
||||||
:badge-label="item.badgeLabel"
|
:label="item.label"
|
||||||
/>
|
:badge-label="item.badgeLabel"
|
||||||
<div class="footer">
|
/>
|
||||||
<slot name="footer" :total-percent="computedData.totalPercentUsage" />
|
<div class="footer">
|
||||||
|
<slot name="footer" :total-percent="computedData.totalPercentUsage" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -19,6 +21,7 @@ import { computed } from "vue";
|
|||||||
import ProgressBar from "@/components/ProgressBar.vue";
|
import ProgressBar from "@/components/ProgressBar.vue";
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
|
id: string;
|
||||||
value: number;
|
value: number;
|
||||||
label?: string;
|
label?: string;
|
||||||
badgeLabel?: string;
|
badgeLabel?: string;
|
||||||
@ -27,7 +30,7 @@ interface Data {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: Array<Data>;
|
data: Array<Data>;
|
||||||
title?: string;
|
nItems?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
@ -45,7 +48,8 @@ const computedData = computed(() => {
|
|||||||
value,
|
value,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((item, nextItem) => nextItem.value - item.value),
|
.sort((item, nextItem) => nextItem.value - item.value)
|
||||||
|
.slice(0, props.nItems ?? _data.length),
|
||||||
totalPercentUsage,
|
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
|
# useFetchStats composable
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
|
<div>
|
||||||
|
<p v-for="(stat, index) in stats" :key="index">
|
||||||
|
{{ stat.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { onMounted } from "vue";
|
||||||
import useFetchStats from "@/composables/fetch-stats-composable";
|
import useFetchStats from "@/composables/fetch-stats-composable";
|
||||||
import { GRANULARITY, type HostStats, type VmStats } from "@/libs/xapi-stats";
|
import { GRANULARITY, type HostStats, type VmStats } from "@/libs/xapi-stats";
|
||||||
|
|
||||||
const vmId = "1d381a66-d1cb-bb7e-50a1-feeab58b293d";
|
const vmStore = useVmStore();
|
||||||
const hostId = "0aea61f4-c9d1-4060-94e8-4eb2024d082c";
|
|
||||||
|
|
||||||
const { stats: vmStats } = useFetchStats<VmStats>(
|
const { register, unregister, stats } = useFetchStats<XenApiVm, VmStats>(
|
||||||
"vm",
|
"vm",
|
||||||
vmId,
|
|
||||||
GRANULARITY.Seconds
|
GRANULARITY.Seconds
|
||||||
);
|
);
|
||||||
|
|
||||||
const { stats: hostStats } = useFetchStats<HostStats>(
|
onMounted(() => {
|
||||||
"host",
|
vmStore.allRecords.forEach(register);
|
||||||
hostId,
|
});
|
||||||
GRANULARITY.Seconds
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { type Ref, ref } from "vue";
|
import { computed, onUnmounted, ref } from "vue";
|
||||||
import { promiseTimeout, useIntervalFn } from "@vueuse/core";
|
import { type Pausable, promiseTimeout, useTimeoutPoll } from "@vueuse/core";
|
||||||
import type { GRANULARITY, XapiStatsResponse } from "@/libs/xapi-stats";
|
import type { GRANULARITY, XapiStatsResponse } from "@/libs/xapi-stats";
|
||||||
|
import type { XenApiHost, XenApiVm } from "@/libs/xen-api";
|
||||||
import { useHostStore } from "@/stores/host.store";
|
import { useHostStore } from "@/stores/host.store";
|
||||||
import { useVmStore } from "@/stores/vm.store";
|
import { useVmStore } from "@/stores/vm.store";
|
||||||
|
|
||||||
@ -9,19 +10,59 @@ const STORES_BY_OBJECT_TYPE = {
|
|||||||
vm: useVmStore,
|
vm: useVmStore,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function useFetchStats<T>(
|
export default function useFetchStats<T extends XenApiHost | XenApiVm, S>(
|
||||||
type: "host" | "vm",
|
type: "host" | "vm",
|
||||||
id: string,
|
|
||||||
granularity: GRANULARITY
|
granularity: GRANULARITY
|
||||||
) {
|
) {
|
||||||
const stats = ref();
|
const stats = ref<
|
||||||
const fetch = STORES_BY_OBJECT_TYPE[type]().getStats;
|
Map<string, { id: string; name: string; stats?: S; pausable: Pausable }>
|
||||||
|
>(new Map());
|
||||||
|
|
||||||
const fetchStats = async () => {
|
const register = (object: T) => {
|
||||||
stats.value = await fetch(id, granularity);
|
if (stats.value.has(object.uuid)) {
|
||||||
await promiseTimeout(stats.value.interval * 1000);
|
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,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
useIntervalFn(fetchStats);
|
|
||||||
|
|
||||||
return { stats } as { stats: Ref<XapiStatsResponse<T>> | 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())),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import { utcParse } from "d3-time-format";
|
import { utcParse } from "d3-time-format";
|
||||||
import humanFormat from "human-format";
|
import humanFormat from "human-format";
|
||||||
import { round } from "lodash-es";
|
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 type { Filter } from "@/types/filter";
|
||||||
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
|
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
|
||||||
import { faFont, faHashtag, faList } from "@fortawesome/free-solid-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(
|
export function sortRecordsByNameLabel(
|
||||||
record1: { name_label: string },
|
record1: { name_label: string },
|
||||||
@ -67,3 +71,55 @@ export const hasEllipsis = (target: Element | undefined | null) =>
|
|||||||
export function percent(currentValue: number, maxValue: number, precision = 2) {
|
export function percent(currentValue: number, maxValue: number, precision = 2) {
|
||||||
return round((currentValue / maxValue) * 100, precision);
|
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 { JSONRPCClient } from "json-rpc-2.0";
|
||||||
import { parseDateTime } from "@/libs/utils";
|
import { buildXoObject, parseDateTime } from "@/libs/utils";
|
||||||
|
|
||||||
export type RawObjectType =
|
export type RawObjectType =
|
||||||
| "Bond"
|
| "Bond"
|
||||||
@ -66,7 +66,7 @@ export interface XenApiRecord {
|
|||||||
uuid: string;
|
uuid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RawXenApiRecord<T extends XenApiRecord> = Omit<T, "$ref">;
|
export type RawXenApiRecord<T extends XenApiRecord> = Omit<T, "$ref">;
|
||||||
|
|
||||||
export interface XenApiPool extends XenApiRecord {
|
export interface XenApiPool extends XenApiRecord {
|
||||||
name_label: string;
|
name_label: string;
|
||||||
@ -232,7 +232,7 @@ export default class XenApi {
|
|||||||
|
|
||||||
const entries = Object.entries(result).map<[string, T]>(([key, entry]) => [
|
const entries = Object.entries(result).map<[string, T]>(([key, entry]) => [
|
||||||
key,
|
key,
|
||||||
{ $ref: key, ...entry } as T,
|
buildXoObject(entry, { opaqueRef: key }) as T,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return new Map(entries);
|
return new Map(entries);
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"change-power-state": "Change power state",
|
"change-power-state": "Change power state",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
|
"cpu-usage":"CPU usage",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"descending": "descending",
|
"descending": "descending",
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"change-power-state": "Changer l'état d'alimentation",
|
"change-power-state": "Changer l'état d'alimentation",
|
||||||
"copy": "Copier",
|
"copy": "Copier",
|
||||||
|
"cpu-usage":"Utilisation CPU",
|
||||||
"dashboard": "Tableau de bord",
|
"dashboard": "Tableau de bord",
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
"descending": "descendant",
|
"descending": "descendant",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { reactive, shallowReactive } from "vue";
|
import { reactive, shallowReactive } from "vue";
|
||||||
|
import { buildXoObject } from "@/libs/utils";
|
||||||
import type { ObjectType, RawObjectType, XenApiRecord } from "@/libs/xen-api";
|
import type { ObjectType, RawObjectType, XenApiRecord } from "@/libs/xen-api";
|
||||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ export const useRecordsStore = defineStore("records", () => {
|
|||||||
opaqueRef: string,
|
opaqueRef: string,
|
||||||
record: T
|
record: T
|
||||||
) {
|
) {
|
||||||
recordsByOpaqueRef.set(opaqueRef, record);
|
recordsByOpaqueRef.set(opaqueRef, buildXoObject(record, { opaqueRef }));
|
||||||
opaqueRefsByObjectType.get(objectType)?.add(opaqueRef);
|
opaqueRefsByObjectType.get(objectType)?.add(opaqueRef);
|
||||||
uuidToOpaqueRefMapping.set(record.uuid, opaqueRef);
|
uuidToOpaqueRefMapping.set(record.uuid, opaqueRef);
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,65 @@
|
|||||||
<div class="pool-dashboard-view">
|
<div class="pool-dashboard-view">
|
||||||
<PoolDashboardStatus class="item" />
|
<PoolDashboardStatus class="item" />
|
||||||
<PoolDashboardStorageUsage class="item" />
|
<PoolDashboardStorageUsage class="item" />
|
||||||
|
<PoolDashboardCpuUsage class="item" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
|
||||||
import PoolDashboardStorageUsage from "@/components/pool/dashboard/PoolDashboardStorageUsage.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>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
|
Loading…
Reference in New Issue
Block a user