feat(lite/dashboard): CPU provisioning (#6601)

This commit is contained in:
Mathieu 2023-03-09 09:54:11 +01:00 committed by GitHub
parent d1b1fa7ffd
commit 1335e12b97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 305 additions and 54 deletions

View File

@ -11,6 +11,7 @@
- Display network throughput chart in pool dashboard (PR [#6610](https://github.com/vatesfr/xen-orchestra/pull/6610))
- Display RAM usage chart in pool dashboard (PR [#6604](https://github.com/vatesfr/xen-orchestra/pull/6604))
- Ability to change the state of a VM (PRs [#6571](https://github.com/vatesfr/xen-orchestra/pull/6571) [#6608](https://github.com/vatesfr/xen-orchestra/pull/6608))
- Display CPU provisioning in pool dashboard (PR [#6601](https://github.com/vatesfr/xen-orchestra/pull/6601))
## **0.1.0**

View File

@ -105,7 +105,7 @@ Use the `busy` prop to display a loader icon.
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
</script>
```

View File

@ -34,7 +34,7 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import AppMenu from "@/components/menu/AppMenu.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useXenApiStore } from "@/stores/xen-api.store";
const router = useRouter();

View File

@ -18,7 +18,7 @@
<script lang="ts" setup>
import AccountButton from "@/components/AccountButton.vue";
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useNavigationStore } from "@/stores/navigation.store";
import { useUiStore } from "@/stores/ui.store";
import { faBars } from "@fortawesome/free-solid-svg-icons";

View File

@ -53,7 +53,7 @@ import UiActionButton from "@/components/ui/UiActionButton.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiFilter from "@/components/ui/UiFilter.vue";
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import type { ActiveSorts, Sorts } from "@/types/sort";

View File

@ -10,7 +10,7 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{

View File

@ -26,7 +26,7 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{

View File

@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { PowerState } from "@/libs/xen-api";
import {
faMoon,

View File

@ -11,7 +11,7 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{

View File

@ -11,13 +11,10 @@
}"
>
<UiProgressBar :value="item.value" color="custom" />
<div class="legend">
<span class="circle" />
{{ item.label }}
<UiBadge class="badge">{{
item.badgeLabel ?? `${item.value}%`
}}</UiBadge>
</div>
<UiProgressLegend
:label="item.label"
:value="item.badgeLabel ?? `${item.value}%`"
/>
</div>
<slot :total-percent="computedData.totalPercentUsage" name="footer" />
</template>
@ -26,9 +23,9 @@
</template>
<script lang="ts" setup>
import UiBadge from "@/components/ui/UiBadge.vue";
import UiProgressBar from "@/components/ui/UiProgressBar.vue";
import { computed } from "vue";
import UiProgressBar from "@/components/ui/progress/UiProgressBar.vue";
import UiProgressLegend from "@/components/ui/progress/UiProgressLegend.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
interface Data {
@ -78,19 +75,6 @@ const computedData = computed(() => {
height: 40px;
}
.legend {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
margin: 1.6em 0;
}
.badge {
font-size: 0.9em;
font-weight: 700;
}
.progress-item:nth-child(1) {
--progress-bar-color: var(--color-extra-blue-d60);
}
@ -114,12 +98,4 @@ const computedData = computed(() => {
--progress-bar-color: var(--color-red-vates-base);
}
}
.circle {
display: inline-block;
width: 1rem;
height: 1rem;
border-radius: 0.5rem;
background-color: var(--progress-bar-color);
}
</style>

View File

@ -35,7 +35,7 @@ import {
} from "vue";
import { faCheck, faCircle, faMinus } from "@fortawesome/free-solid-svg-icons";
import { useVModel } from "@vueuse/core";
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
// Temporary workaround for https://github.com/vuejs/core/issues/4294
interface Props extends Omit<InputHTMLAttributes, ""> {

View File

@ -65,7 +65,7 @@ import type { Color } from "@/types";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
import { useTextareaAutosize, useVModel } from "@vueuse/core";
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
// Temporary workaround for https://github.com/vuejs/core/issues/4294
interface Props extends Omit<InputHTMLAttributes, ""> {

View File

@ -19,7 +19,7 @@
<script lang="ts" setup>
import { computed, provide, useSlots } from "vue";
import { faCircleExclamation } from "@fortawesome/free-solid-svg-icons";
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
const slots = useSlots();

View File

@ -7,7 +7,7 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{

View File

@ -21,7 +21,7 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import type { RouteLocationRaw } from "vue-router";

View File

@ -10,7 +10,7 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{

View File

@ -39,7 +39,7 @@ import { faAngleDown, faAngleRight } from "@fortawesome/free-solid-svg-icons";
import { noop } from "@vueuse/core";
import AppMenu from "@/components/menu/AppMenu.vue";
import MenuTrigger from "@/components/menu/MenuTrigger.vue";
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
const props = defineProps<{
icon?: IconDefinition;

View File

@ -7,7 +7,7 @@
<script lang="ts" setup>
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
defineProps<{
active?: boolean;

View File

@ -0,0 +1,100 @@
<template>
<UiCard>
<UiCardTitle>
{{ $t("cpu-provisioning") }}
<template #right>
<!-- TODO: add a tooltip for the warning icon -->
<UiStatusIcon v-if="state !== 'success'" :state="state" />
</template>
</UiCardTitle>
<div v-if="isReady" class="progress-item" :class="state">
<UiProgressBar color="custom" :value="value" :max-value="maxValue" />
<UiProgressScale :max-value="maxValue" unit="%" :steps="1" />
<UiProgressLegend :label="$t('vcpus')" :value="`${value}%`" />
<UiCardFooter>
<template #left>
<p>{{ $t("vcpus-used") }}</p>
<p class="footer-value">{{ nVCpuInUse }}</p>
</template>
<template #right>
<p>{{ $t("total-cpus") }}</p>
<p class="footer-value">{{ nPCpu }}</p>
</template>
</UiCardFooter>
</div>
<UiSpinner v-else class="spinner" />
</UiCard>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { storeToRefs } from "pinia";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardFooter from "@/components/ui/UiCardFooter.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiProgressBar from "@/components/ui/progress/UiProgressBar.vue";
import UiProgressLegend from "@/components/ui/progress/UiProgressLegend.vue";
import UiProgressScale from "@/components/ui/progress/UiProgressScale.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import UiStatusIcon from "@/components/ui/icon/UiStatusIcon.vue";
import { isHostRunning, percent } from "@/libs/utils";
import { useHostStore } from "@/stores/host.store";
import { useVmMetricsStore } from "@/stores/vm-metrics.store";
import { useVmStore } from "@/stores/vm.store";
const ACTIVE_STATES = new Set(["Running", "Paused"]);
const { allRecords: hosts, isReady: hostStoreIsReady } = storeToRefs(
useHostStore()
);
const { allRecords: vms, isReady: vmStoreIsReady } = storeToRefs(useVmStore());
const vmMetricsStore = useVmMetricsStore();
const nPCpu = computed(() =>
hosts.value.reduce(
(total, host) =>
isHostRunning(host) ? total + Number(host.cpu_info.cpu_count) : total,
0
)
);
const nVCpuInUse = computed(() =>
vms.value.reduce(
(total, vm) =>
ACTIVE_STATES.has(vm.power_state)
? total + vmMetricsStore.getRecord(vm.metrics).VCPUs_number
: total,
0
)
);
const value = computed(() =>
Math.round(percent(nVCpuInUse.value, nPCpu.value))
);
const maxValue = computed(() => Math.ceil(value.value / 100) * 100);
const state = computed(() => (value.value > 100 ? "warning" : "success"));
const isReady = computed(
() => vmStoreIsReady.value && vmMetricsStore.isReady && hostStoreIsReady.value
);
</script>
<style lang="postcss" scoped>
.progress-item {
margin-top: 2.6rem;
--progress-bar-height: 1.2rem;
--progress-bar-color: var(--color-extra-blue-base);
--progress-bar-background-color: var(--color-blue-scale-400);
&.warning {
--progress-bar-color: var(--color-orange-world-base);
--footer-value-color: var(--color-orange-world-base);
}
& .footer-value {
color: var(--footer-value-color);
}
}
.spinner {
color: var(--color-extra-blue-base);
display: flex;
margin: 2.6rem auto auto auto;
width: 40px;
height: 40px;
}
</style>

View File

@ -32,7 +32,7 @@
<script lang="ts" setup>
import RelativeTime from "@/components/RelativeTime.vue";
import UiProgressBar from "@/components/ui/UiProgressBar.vue";
import UiProgressBar from "@/components/ui/progress/UiProgressBar.vue";
import { parseDateTime } from "@/libs/utils";
import type { XenApiTask } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";

View File

@ -18,7 +18,7 @@
<script lang="ts" setup>
import { computed, inject, unref } from "vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
const props = withDefaults(
defineProps<{

View File

@ -7,7 +7,7 @@
<script lang="ts" setup>
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
defineProps<{
icon?: IconDefinition;

View File

@ -18,7 +18,7 @@ import UiSpinner from "@/components/ui/UiSpinner.vue";
import { computed, inject, unref } from "vue";
import type { Color } from "@/types";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
const props = withDefaults(
defineProps<{

View File

@ -0,0 +1,32 @@
<template>
<div class="footer">
<div class="footer-card">
<slot name="left" />
</div>
<div class="footer-card">
<slot name="right" />
</div>
</div>
</template>
<script lang="ts" setup></script>
<style lang="postcss" scoped>
.footer {
color: var(--color-blue-scale-200);
display: flex;
font-size: 14px;
justify-content: space-between;
}
.footer-card {
display: flex;
text-transform: uppercase;
width: 45%;
justify-content: space-between;
}
:deep(.footer-card) p {
font-weight: 700;
}
</style>

View File

@ -10,7 +10,7 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { faRemove } from "@fortawesome/free-solid-svg-icons";
const emit = defineEmits<{

View File

@ -34,7 +34,7 @@
<script lang="ts" setup>
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faXmark } from "@fortawesome/free-solid-svg-icons";

View File

@ -0,0 +1,49 @@
<template>
<UiIcon :icon="icon" class="icon" :class="props.state" />
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { computed } from "vue";
import {
faCheckCircle,
faInfoCircle,
faWarning,
} from "@fortawesome/free-solid-svg-icons";
import type { Color } from "@/types";
const props = defineProps<{
state: Color;
}>();
const icon = computed(() => {
switch (props.state) {
case "error":
case "warning":
return faWarning;
case "info":
return faInfoCircle;
default:
return faCheckCircle;
}
});
</script>
<style scoped lang="postcss">
.icon {
color: var(--icon-color);
&.error {
--icon-color: var(--color-red-vates-base);
}
&.warning {
--icon-color: var(--color-orange-world-base);
}
&.info {
--icon-color: var(--color-extra-blue-base);
}
&.success {
--icon-color: var(--color-green-infra-base);
}
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<div class="legend">
<span class="circle" />
<slot name="label">{{ label }}</slot>
<UiBadge class="badge">
<slot name="value">{{ value }}</slot>
</UiBadge>
</div>
</template>
<script lang="ts" setup>
import UiBadge from "@/components/ui/UiBadge.vue";
defineProps<{
label?: string;
value?: string;
}>();
</script>
<style scoped lang="postcss">
.badge {
font-size: 0.9em;
font-weight: 700;
}
.circle {
display: inline-block;
width: 1rem;
height: 1rem;
border-radius: 0.5rem;
background-color: var(--progress-bar-color);
}
.legend {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
margin: 1.6em 0;
}
</style>

View File

@ -0,0 +1,32 @@
<template>
<p class="unit">
<span>0{{ unit }}</span>
<span v-for="step in steps" :key="step">
{{ Math.round((maxValue / (steps + 1)) * step) }}{{ unit }}
</span>
<span>{{ maxValue }}{{ unit }}</span>
</p>
</template>
<script lang="ts" setup>
withDefaults(
defineProps<{
maxValue?: number;
steps?: number;
unit?: string;
}>(),
{ maxValue: 100, steps: 1 }
);
</script>
<style lang="postcss">
.unit {
color: var(--color-blue-scale-300);
display: flex;
font-size: 12px;
font-weight: 400;
justify-content: space-between;
letter-spacing: 0.04em;
line-height: 16px;
}
</style>

View File

@ -118,7 +118,7 @@ import MenuItem from "@/components/menu/MenuItem.vue";
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import TitleBar from "@/components/TitleBar.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { isHostRunning } from "@/libs/utils";
import { useHostStore } from "@/stores/host.store";
import { usePoolStore } from "@/stores/pool.store";

View File

@ -70,6 +70,9 @@ export interface XenApiRecord {
export type RawXenApiRecord<T extends XenApiRecord> = Omit<T, "$ref">;
export interface XenApiPool extends XenApiRecord {
cpu_info: {
cpu_count: string;
};
master: string;
name_label: string;
}
@ -90,6 +93,8 @@ export interface XenApiSr extends XenApiRecord {
export interface XenApiVm extends XenApiRecord {
current_operations: Record<string, string>;
guest_metrics: string;
metrics: string;
name_label: string;
name_description: string;
power_state: PowerState;
@ -98,6 +103,7 @@ export interface XenApiVm extends XenApiRecord {
is_control_domain: boolean;
is_a_snapshot: boolean;
is_a_template: boolean;
VCPUs_at_startup: number;
}
export interface XenApiConsole extends XenApiRecord {
@ -111,7 +117,9 @@ export interface XenApiHostMetrics extends XenApiRecord {
memory_total: number;
}
export type XenApiVmMetrics = XenApiRecord;
export interface XenApiVmMetrics extends XenApiRecord {
VCPUs_number: number;
}
export type XenApiVmGuestMetrics = XenApiRecord;

View File

@ -17,6 +17,7 @@
"community": "Community",
"community-name": "{name} community",
"copy": "Copy",
"cpu-provisioning": "CPU provisioning",
"cpu-usage": "CPU usage",
"dashboard": "Dashboard",
"delete": "Delete",
@ -96,10 +97,13 @@
"theme-dark": "Dark",
"theme-light": "Light",
"top-#": "Top {n}",
"total-cpus": "Total CPUs",
"total-free": "Total free",
"total-used": "Total used",
"unreachable-hosts": "Unreachable hosts",
"unreachable-hosts-reload-page": "Done, reload the page",
"vcpus": "vCPUs",
"vcpus-used": "vCPUs used",
"version": "Version",
"vms": "VMs"
}

View File

@ -17,6 +17,7 @@
"community": "Communauté",
"community-name": "Communauté {name}",
"copy": "Copier",
"cpu-provisioning": "Provisionnement CPU",
"cpu-usage": "Utilisation CPU",
"dashboard": "Tableau de bord",
"delete": "Supprimer",
@ -96,10 +97,13 @@
"theme-dark": "Sombre",
"theme-light": "Clair",
"top-#": "Top {n}",
"total-cpus": "Total CPUs",
"total-free": "Total libre",
"total-used": "Total utilisé",
"unreachable-hosts": "Hôtes inaccessibles",
"unreachable-hosts-reload-page": "C'est fait. Rafraîchir la page",
"vcpus": "vCPUs",
"vcpus-used": "vCPUs utilisés",
"version": "Version",
"vms": "VMs"
}

View File

@ -12,6 +12,9 @@
<div class="item">
<PoolDashboardRamUsage />
</div>
<div class="item">
<PoolDashboardCpuProvisionning />
</div>
<div class="item">
<PoolDashboardNetworkChart />
</div>
@ -33,6 +36,7 @@ import { computed, onMounted, provide, watch } from "vue";
import PoolCpuUsageChart from "@/components/pool/dashboard/cpuUsage/PoolCpuUsageChart.vue";
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.vue";
import PoolDashboardNetworkChart from "@/components/pool/dashboard/PoolDashboardNetworkChart.vue";
import PoolDashboardCpuProvisionning from "@/components/pool/dashboard/PoolDashboardCpuProvisionning.vue";
import PoolDashboardRamUsage from "@/components/pool/dashboard/PoolDashboardRamUsage.vue";
import PoolDashboardRamUsageChart from "@/components/pool/dashboard/ramUsage/PoolRamUsage.vue";
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";