feat(lite): display loading icon and error message when data is not fetched (#6775)

This commit is contained in:
rbarhtaoui
2023-10-03 10:03:44 +02:00
committed by GitHub
parent 2a5e09719e
commit e853f9d04f
14 changed files with 194 additions and 134 deletions

View File

@@ -2,12 +2,7 @@
```vue
<template>
<LinearChart
title="Chart title"
subtitle="Chart subtitle"
:data="data"
:value-formatter="customValueFormatter"
/>
<LinearChart :data="data" :value-formatter="customValueFormatter" />
</template>
<script lang="ts" setup>

View File

@@ -1,12 +1,8 @@
<template>
<UiCard class="linear-chart">
<VueCharts :option="option" autoresize class="chart" />
<slot name="summary" />
</UiCard>
<VueCharts :option="option" autoresize class="chart" />
</template>
<script lang="ts" setup>
import UiCard from "@/components/ui/UiCard.vue";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_CHART_VALUE_FORMATTER } from "@/types/injection-keys";
import { utcFormat } from "d3-time-format";
@@ -15,7 +11,6 @@ import { LineChart } from "echarts/charts";
import {
GridComponent,
LegendComponent,
TitleComponent,
TooltipComponent,
} from "echarts/components";
import { use } from "echarts/core";
@@ -26,8 +21,6 @@ import VueCharts from "vue-echarts";
const Y_AXIS_MAX_VALUE = 200;
const props = defineProps<{
title?: string;
subtitle?: string;
data: LinearChartData;
valueFormatter?: ValueFormatter;
maxValue?: number;
@@ -52,15 +45,10 @@ use([
LineChart,
GridComponent,
TooltipComponent,
TitleComponent,
LegendComponent,
]);
const option = computed<EChartsOption>(() => ({
title: {
text: props.title,
subtext: props.subtitle,
},
legend: {
data: props.data.map((series) => series.label),
},

View File

@@ -1,33 +1,44 @@
<template>
<!-- TODO: add a loader when data is not fully loaded or undefined -->
<!-- TODO: add small loader with tooltips when stats can be expired -->
<!-- TODO: display the NoData component in case of a data recovery error -->
<LinearChart
:data="data"
:max-value="customMaxValue"
:subtitle="$t('last-week')"
:title="$t('network-throughput')"
:value-formatter="customValueFormatter"
/>
<UiCard class="linear-chart" :color="hasError ? 'error' : undefined">
<UiCardTitle>{{ $t("network-throughput") }}</UiCardTitle>
<UiCardTitle :level="UiCardTitleLevel.Subtitle">
{{ $t("last-week") }}
</UiCardTitle>
<NoDataError v-if="hasError" />
<UiCardSpinner v-else-if="isLoading" />
<LinearChart
v-else
:data="data"
:max-value="customMaxValue"
:value-formatter="customValueFormatter"
/>
</UiCard>
</template>
<script lang="ts" setup>
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import { computed, defineAsyncComponent, inject } from "vue";
import { map } from "lodash-es";
import { useI18n } from "vue-i18n";
import { formatSize } from "@/libs/utils";
import type { HostStats } from "@/libs/xapi-stats";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import type { LinearChartData } from "@/types/chart";
import { map } from "lodash-es";
import NoDataError from "@/components/NoDataError.vue";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import { UiCardTitleLevel } from "@/types/enums";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const LinearChart = defineAsyncComponent(
() => import("@/components/charts/LinearChart.vue")
);
const { t } = useI18n();
const hostLastWeekStats = inject(IK_HOST_LAST_WEEK_STATS);
const { hasError, isFetching } = useHostCollection();
const data = computed<LinearChartData>(() => {
const stats = hostLastWeekStats?.stats?.value;
@@ -82,6 +93,25 @@ const data = computed<LinearChartData>(() => {
];
});
const isStatFetched = computed(() => {
const stats = hostLastWeekStats?.stats?.value;
if (stats === undefined) {
return false;
}
return stats.every((host) => {
const hostStats = host.stats;
return (
hostStats != null &&
Object.values(hostStats.pifs["rx"])[0].length +
Object.values(hostStats.pifs["tx"])[0].length ===
data.value[0].data.length + data.value[1].data.length
);
});
});
const isLoading = computed(() => isFetching.value || !isStatFetched.value);
// TODO: improve the way to get the max value of graph
// See: https://github.com/vatesfr/xen-orchestra/pull/6610/files#r1072237279
const customMaxValue = computed(

View File

@@ -1,22 +1,23 @@
<template>
<UiCardTitle
:level="UiCardTitleLevel.SubtitleWithUnderline"
:left="$t('hosts')"
:right="$t('top-#', { n: N_ITEMS })"
subtitle
/>
<NoDataError v-if="hasError" />
<UsageBar v-else :data="statFetched ? data : undefined" :n-items="N_ITEMS" />
</template>
<script lang="ts" setup>
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { computed, inject, type ComputedRef } from "vue";
import { getAvgCpuUsage } from "@/libs/utils";
import { IK_HOST_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { UiCardTitleLevel } from "@/types/enums";
import UsageBar from "@/components/UsageBar.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
const { hasError } = useHostCollection();

View File

@@ -1,24 +1,33 @@
<template>
<!-- TODO: add a loader when data is not fully loaded or undefined -->
<!-- TODO: add small loader with tooltips when stats can be expired -->
<!-- TODO: Display the NoDataError component in case of a data recovery error -->
<LinearChart
:data="data"
:max-value="customMaxValue"
:subtitle="$t('last-week')"
:title="$t('pool-cpu-usage')"
:value-formatter="customValueFormatter"
/>
<UiCard class="linear-chart" :color="hasError ? 'error' : undefined">
<UiCardTitle>{{ $t("pool-cpu-usage") }}</UiCardTitle>
<UiCardTitle :level="UiCardTitleLevel.Subtitle">
{{ $t("last-week") }}
</UiCardTitle>
<NoDataError v-if="hasError" />
<UiCardSpinner v-else-if="isLoading" />
<LinearChart
v-else
:data="data"
:max-value="customMaxValue"
:value-formatter="customValueFormatter"
/>
</UiCard>
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/stores/xen-api/host.store";
import type { HostStats } from "@/libs/xapi-stats";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import { sumBy } from "lodash-es";
import { computed, defineAsyncComponent, inject } from "vue";
import type { HostStats } from "@/libs/xapi-stats";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import NoDataError from "@/components/NoDataError.vue";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import { sumBy } from "lodash-es";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import { UiCardTitleLevel } from "@/types/enums";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useI18n } from "vue-i18n";
const LinearChart = defineAsyncComponent(
@@ -29,8 +38,7 @@ const { t } = useI18n();
const hostLastWeekStats = inject(IK_HOST_LAST_WEEK_STATS);
const { records: hosts } = useHostCollection();
const { records: hosts, isFetching, hasError } = useHostCollection();
const customMaxValue = computed(
() => 100 * sumBy(hosts.value, (host) => +host.cpu_info.cpu_count)
);
@@ -79,6 +87,22 @@ const data = computed<LinearChartData>(() => {
},
];
});
const isStatFetched = computed(() => {
const stats = hostLastWeekStats?.stats?.value;
if (stats === undefined) {
return false;
}
return stats.every((host) => {
const hostStats = host.stats;
return (
hostStats != null &&
Object.values(hostStats.cpus)[0].length === data.value[0].data.length
);
});
});
const isLoading = computed(() => isFetching.value || !isStatFetched.value);
const customValueFormatter: ValueFormatter = (value) => `${value}%`;
</script>

View File

@@ -1,6 +1,6 @@
<template>
<UiCardTitle
subtitle
:level="UiCardTitleLevel.SubtitleWithUnderline"
:left="$t('vms')"
:right="$t('top-#', { n: N_ITEMS })"
/>
@@ -9,6 +9,7 @@
</template>
<script lang="ts" setup>
import { type ComputedRef, computed, inject } from "vue";
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
@@ -16,7 +17,7 @@ import { useVmCollection } from "@/stores/xen-api/vm.store";
import { getAvgCpuUsage } from "@/libs/utils";
import { IK_VM_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
import { UiCardTitleLevel } from "@/types/enums";
const { hasError } = useVmCollection();

View File

@@ -1,6 +1,6 @@
<template>
<UiCardTitle
subtitle
:level="UiCardTitleLevel.SubtitleWithUnderline"
:left="$t('hosts')"
:right="$t('top-#', { n: N_ITEMS })"
/>
@@ -13,6 +13,7 @@ import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { IK_HOST_STATS } from "@/types/injection-keys";
import { type ComputedRef, computed, inject } from "vue";
import { UiCardTitleLevel } from "@/types/enums";
import UsageBar from "@/components/UsageBar.vue";
import { formatSize, parseRamUsage } from "@/libs/utils";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";

View File

@@ -1,37 +1,43 @@
<template>
<!-- TODO: add a loader when data is not fully loaded or undefined -->
<!-- TODO: add small loader with tooltips when stats can be expired -->
<!-- TODO: display the NoDataError component in case of a data recovery error -->
<LinearChart
:data="data"
:max-value="customMaxValue"
:subtitle="$t('last-week')"
:title="$t('pool-ram-usage')"
:value-formatter="customValueFormatter"
>
<template #summary>
<SizeStatsSummary :size="currentData.size" :usage="currentData.usage" />
</template>
</LinearChart>
<UiCard class="linear-chart" :color="hasError ? 'error' : undefined">
<UiCardTitle>{{ $t("pool-ram-usage") }}</UiCardTitle>
<UiCardTitle :level="UiCardTitleLevel.Subtitle">
{{ $t("last-week") }}
</UiCardTitle>
<NoDataError v-if="hasError" />
<UiCardSpinner v-else-if="isLoading" />
<LinearChart
v-else
:data="data"
:max-value="customMaxValue"
:value-formatter="customValueFormatter"
/>
<SizeStatsSummary :size="currentData.size" :usage="currentData.usage" />
</UiCard>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, inject } from "vue";
import { formatSize } from "@/libs/utils";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import type { LinearChartData } from "@/types/chart";
import NoDataError from "@/components/NoDataError.vue";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import SizeStatsSummary from "@/components/ui/SizeStatsSummary.vue";
import { sumBy } from "lodash-es";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import { UiCardTitleLevel } from "@/types/enums";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useHostMetricsCollection } from "@/stores/xen-api/host-metrics.store";
import { formatSize } from "@/libs/utils";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import { sumBy } from "lodash-es";
import { computed, defineAsyncComponent, inject } from "vue";
import { useI18n } from "vue-i18n";
const LinearChart = defineAsyncComponent(
() => import("@/components/charts/LinearChart.vue")
);
const { runningHosts } = useHostCollection();
const { runningHosts, isFetching, hasError } = useHostCollection();
const { getHostMemory } = useHostMetricsCollection();
const { t } = useI18n();
@@ -92,6 +98,23 @@ const data = computed<LinearChartData>(() => {
];
});
const customValueFormatter: ValueFormatter = (value) =>
String(formatSize(value));
const isStatFetched = computed(() => {
const stats = hostLastWeekStats?.stats?.value;
if (stats === undefined) {
return false;
}
return stats.every((host) => {
const hostStats = host.stats;
return (
hostStats != null && hostStats.memory.length === data.value[0].data.length
);
});
});
const isLoading = computed(
() => (isFetching.value && !hasError.value) || !isStatFetched.value
);
const customValueFormatter = (value: number) => String(formatSize(value));
</script>

View File

@@ -1,6 +1,6 @@
<template>
<UiCardTitle
subtitle
:level="UiCardTitleLevel.SubtitleWithUnderline"
:left="$t('vms')"
:right="$t('top-#', { n: N_ITEMS })"
/>
@@ -9,14 +9,15 @@
</template>
<script lang="ts" setup>
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { computed, inject, type ComputedRef } from "vue";
import { formatSize, parseRamUsage } from "@/libs/utils";
import { IK_VM_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { UiCardTitleLevel } from "@/types/enums";
import UsageBar from "@/components/UsageBar.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
const { hasError } = useVmCollection();

View File

@@ -1,35 +1,40 @@
<template>
<div :class="{ subtitle }" class="ui-section-title">
<component
:is="subtitle ? 'h5' : 'h4'"
v-if="$slots.default || left"
class="left"
>
<div :class="['ui-section-title', tags.left]">
<component :is="tags.left" v-if="$slots.default || left" class="left">
<slot>{{ left }}</slot>
<UiCounter class="count" v-if="count > 0" :value="count" color="info" />
</component>
<component
:is="subtitle ? 'h6' : 'h5'"
v-if="$slots.right || right"
class="right"
>
<component :is="tags.right" v-if="$slots.right || right" class="right">
<slot name="right">{{ right }}</slot>
</component>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import UiCounter from "@/components/ui/UiCounter.vue";
import { UiCardTitleLevel } from "@/types/enums";
withDefaults(
const props = withDefaults(
defineProps<{
subtitle?: boolean;
count?: number;
level?: UiCardTitleLevel;
left?: string;
right?: string;
count?: number;
}>(),
{ count: 0 }
{ count: 0, level: UiCardTitleLevel.Title }
);
const tags = computed(() => {
switch (props.level) {
case UiCardTitleLevel.Subtitle:
return { left: "h6", right: "h6" };
case UiCardTitleLevel.SubtitleWithUnderline:
return { left: "h5", right: "h6" };
default:
return { left: "h4", right: "h5" };
}
});
</script>
<style lang="postcss" scoped>
@@ -37,7 +42,6 @@ withDefaults(
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2rem;
--section-title-left-size: 2rem;
--section-title-left-color: var(--color-blue-scale-100);
@@ -46,9 +50,17 @@ withDefaults(
--section-title-right-color: var(--color-extra-blue-base);
--section-title-right-weight: 700;
&.subtitle {
border-bottom: 1px solid var(--color-extra-blue-base);
&.h6 {
margin-bottom: 1rem;
--section-title-left-size: 1.5rem;
--section-title-left-color: var(--color-blue-scale-300);
--section-title-left-weight: 400;
}
&.h5 {
margin-top: 2rem;
margin-bottom: 1rem;
border-bottom: 1px solid var(--color-extra-blue-base);
--section-title-left-size: 1.6rem;
--section-title-left-color: var(--color-extra-blue-base);
--section-title-left-weight: 700;

View File

@@ -10,8 +10,7 @@ export const useChartTheme = () => {
const getColors = () => ({
background: style.getPropertyValue("--background-color-primary"),
title: style.getPropertyValue("--color-blue-scale-100"),
subtitle: style.getPropertyValue("--color-blue-scale-300"),
text: style.getPropertyValue("--color-blue-scale-300"),
splitLine: style.getPropertyValue("--color-blue-scale-400"),
primary: style.getPropertyValue("--color-extra-blue-base"),
secondary: style.getPropertyValue("--color-orange-world-base"),
@@ -28,24 +27,10 @@ export const useChartTheme = () => {
backgroundColor: colors.value.background,
textStyle: {},
grid: {
top: 80,
top: 40,
left: 80,
right: 20,
},
title: {
textStyle: {
color: colors.value.title,
fontFamily: "Poppins, sans-serif",
fontWeight: 500,
fontSize: 20,
},
subtextStyle: {
color: colors.value.subtitle,
fontFamily: "Poppins, sans-serif",
fontWeight: 400,
fontSize: 14,
},
},
line: {
itemStyle: {
borderWidth: 2,
@@ -235,7 +220,7 @@ export const useChartTheme = () => {
},
axisLabel: {
show: true,
color: colors.value.subtitle,
color: colors.value.text,
},
splitLine: {
show: true,
@@ -295,7 +280,7 @@ export const useChartTheme = () => {
},
axisLabel: {
show: true,
color: colors.value.subtitle,
color: colors.value.text,
},
splitLine: {
show: true,
@@ -325,7 +310,7 @@ export const useChartTheme = () => {
left: "right",
top: "bottom",
textStyle: {
color: colors.value.subtitle,
color: colors.value.text,
},
},
tooltip: {

View File

@@ -16,8 +16,6 @@ type LinearChartData = {
```vue-template
<LinearChart
title="Chart title"
subtitle="Chart subtitle"
:data="data"
/>
```

View File

@@ -1,8 +1,6 @@
<template>
<ComponentStory
:params="[
prop('title').preset('Chart title').widget(),
prop('subtitle').preset('Here is a subtitle').widget(),
prop('data')
.preset(data)
.required()
@@ -58,8 +56,6 @@ const data: LinearChartData = [
const presets = {
"Network bandwidth": {
props: {
title: "Network bandwidth",
subtitle: "Last week",
"value-formatter": byteFormatter,
"max-value": 500000000,
data: [

View File

@@ -0,0 +1,5 @@
export enum UiCardTitleLevel {
Title,
Subtitle,
SubtitleWithUnderline,
}