feat(web-stack): introducing defineRequest + convert site dashboard (#8839)

This commit is contained in:
Thierry Goettelmann
2025-07-29 17:59:27 +02:00
committed by GitHub
parent 1045016545
commit 13f406fe70
24 changed files with 372 additions and 186 deletions

View File

@@ -171,7 +171,6 @@ module.exports = {
'vue/no-empty-component-block': 'error', 'vue/no-empty-component-block': 'error',
'vue/no-multiple-objects-in-class': 'error', 'vue/no-multiple-objects-in-class': 'error',
'vue/no-ref-object-reactivity-loss': 'error', 'vue/no-ref-object-reactivity-loss': 'error',
'vue/no-required-prop-with-default': 'error',
'vue/no-static-inline-styles': 'error', 'vue/no-static-inline-styles': 'error',
'vue/no-template-target-blank': 'error', 'vue/no-template-target-blank': 'error',
'vue/no-undef-components': ['error', { ignorePatterns: ['RouterLink', 'RouterView', 'I18nT'] }], 'vue/no-undef-components': ['error', { ignorePatterns: ['RouterLink', 'RouterView', 'I18nT'] }],

View File

@@ -0,0 +1,64 @@
import { useTimeoutPoll } from '@vueuse/core'
import type { MaybeRefOrGetter } from '@vueuse/shared'
// eslint-disable-next-line import/namespace,import/default,import/no-named-as-default,import/no-named-as-default-member -- https://github.com/pamelafox/ndjson-readablestream/pull/13
import readNDJSONStream from 'ndjson-readablestream'
import { computed, isRef, toValue, watch } from 'vue'
export function defineRequest<TState>(options: {
url: string
state: () => TState
onDataReceived: (state: TState, data: unknown) => void
}): () => TState
export function defineRequest<TArgs extends any[], TState>(options: {
url: (...args: TArgs) => string
state: () => TState
onDataReceived: (state: TState, data: unknown) => void
onUrlChange: (state: TState) => void
}): (...args: { [K in keyof TArgs]: MaybeRefOrGetter<TArgs[K]> }) => TState
export function defineRequest<TArgs extends any[], TState>(options: {
url: string | ((...args: TArgs) => string)
state: () => TState
onDataReceived: (state: TState, data: unknown) => void
onUrlChange?: (state: TState) => void
}) {
return function useRequest(...args: { [K in keyof TArgs]: MaybeRefOrGetter<TArgs[K]> }) {
const urlOption = options.url
const url = typeof urlOption === 'function' ? computed(() => urlOption(...(args.map(toValue) as TArgs))) : urlOption
const state = options.state()
async function execute() {
const response = await fetch(toValue(url))
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.statusText}`)
}
if (!response.body) {
throw new Error('Response body is empty')
}
for await (const event of readNDJSONStream(response.body)) {
options.onDataReceived(state, event)
}
}
const { resume, pause } = useTimeoutPoll(execute, 10000, {
immediate: true,
immediateCallback: true,
})
if (isRef(url)) {
watch(url, () => {
pause()
options.onUrlChange?.(state)
resume()
})
}
return state
}
}

View File

@@ -26,6 +26,7 @@
"human-format": "^1.2.1", "human-format": "^1.2.1",
"iterable-backoff": "^0.1.0", "iterable-backoff": "^0.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"ndjson-readablestream": "^1.2.0",
"placement.js": "^1.0.0-beta.5", "placement.js": "^1.0.0-beta.5",
"simple-icons": "^14.14.0", "simple-icons": "^14.14.0",
"vue-echarts": "^6.6.8" "vue-echarts": "^6.6.8"

View File

@@ -36,6 +36,7 @@
"d3-time-format": "^4.1.0", "d3-time-format": "^4.1.0",
"human-format": "^1.2.1", "human-format": "^1.2.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"ndjson-readablestream": "^1.2.0",
"npm-run-all2": "^7.0.2", "npm-run-all2": "^7.0.2",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"postcss": "^8.5.3", "postcss": "^8.5.3",

View File

@@ -45,7 +45,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useDashboardStore } from '@/stores/xo-rest-api/dashboard.store' import type { XoDashboard } from '@/types/xo/dashboard.type.ts'
import VtsBackupState from '@core/components/backup-state/VtsBackupState.vue' import VtsBackupState from '@core/components/backup-state/VtsBackupState.vue'
import VtsDataTable from '@core/components/data-table/VtsDataTable.vue' import VtsDataTable from '@core/components/data-table/VtsDataTable.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue' import VtsIcon from '@core/components/icon/VtsIcon.vue'
@@ -61,13 +61,15 @@ import { faFloppyDisk, faSquareCaretDown } from '@fortawesome/free-solid-svg-ico
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { issues } = defineProps<{
issues: NonNullable<XoDashboard['backups']>['issues'] | undefined
}>()
const { t } = useI18n() const { t } = useI18n()
const { record } = useDashboardStore().subscribe() const areBackupIssuesReady = computed(() => issues !== undefined)
const areBackupIssuesReady = computed(() => record.value?.backups?.issues !== undefined) const backupIssues = computed(() => issues ?? [])
const backupIssues = computed(() => record.value?.backups?.issues ?? [])
const logLabels = [t('last'), t('2nd-last'), t('3rd-last')] const logLabels = [t('last'), t('2nd-last'), t('3rd-last')]

View File

@@ -9,20 +9,20 @@
<VtsStackedBarWithLegend :max-value="maxValue" :segments /> <VtsStackedBarWithLegend :max-value="maxValue" :segments />
<div class="numbers"> <div class="numbers">
<UiCardNumbers <UiCardNumbers
:value="backupRepositories?.used?.value" :value="repositories?.used?.value"
:unit="backupRepositories?.used?.prefix" :unit="repositories?.used?.prefix"
:label="t('used')" :label="t('used')"
size="medium" size="medium"
/> />
<UiCardNumbers <UiCardNumbers
:value="backupRepositories?.available?.value" :value="repositories?.available?.value"
:unit="backupRepositories?.available?.prefix" :unit="repositories?.available?.prefix"
:label="t('available')" :label="t('available')"
size="medium" size="medium"
/> />
<UiCardNumbers <UiCardNumbers
:value="backupRepositories?.total?.value" :value="repositories?.total?.value"
:unit="backupRepositories?.total?.prefix" :unit="repositories?.total?.prefix"
:label="t('total')" :label="t('total')"
size="medium" size="medium"
/> />
@@ -32,7 +32,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useDashboardStore } from '@/stores/xo-rest-api/dashboard.store' import type { BackupRepositories } from '@/requests/use-site-dashboard.request.ts'
import VtsStackedBarWithLegend, { import VtsStackedBarWithLegend, {
type StackedBarWithLegendProps, type StackedBarWithLegendProps,
} from '@core/components/stacked-bar-with-legend/VtsStackedBarWithLegend.vue' } from '@core/components/stacked-bar-with-legend/VtsStackedBarWithLegend.vue'
@@ -42,30 +42,32 @@ import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import { computed, type ComputedRef } from 'vue' import { computed, type ComputedRef } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { repositories } = defineProps<{
repositories: BackupRepositories | undefined
}>()
const { t } = useI18n() const { t } = useI18n()
const { backupRepositories } = useDashboardStore().subscribe() const areBackupRepositoriesReady = computed(() => repositories !== undefined)
const areBackupRepositoriesReady = computed(() => backupRepositories.value !== undefined)
const segments: ComputedRef<StackedBarWithLegendProps['segments']> = computed(() => [ const segments: ComputedRef<StackedBarWithLegendProps['segments']> = computed(() => [
{ {
label: t('xo-backups'), label: t('xo-backups'),
value: backupRepositories.value?.backups?.value ?? 0, value: repositories?.backups.value ?? 0,
accent: 'info', accent: 'info',
unit: backupRepositories.value?.backups?.prefix, unit: repositories?.backups.prefix,
}, },
{ {
label: t('other'), label: t('other'),
value: backupRepositories.value?.other?.value ?? 0, value: repositories?.other?.value ?? 0,
accent: 'warning', accent: 'warning',
unit: backupRepositories.value?.other?.prefix, unit: repositories?.other?.prefix,
}, },
]) ])
const maxValue = computed(() => ({ const maxValue = computed(() => ({
value: backupRepositories.value?.total?.value, value: repositories?.total?.value,
unit: backupRepositories.value?.total?.prefix, unit: repositories?.total?.prefix,
})) }))
</script> </script>

View File

@@ -2,10 +2,10 @@
<UiCard> <UiCard>
<UiCardTitle>{{ t('backups') }}</UiCardTitle> <UiCardTitle>{{ t('backups') }}</UiCardTitle>
<VtsLoadingHero v-if="!areBackupsReady" type="card" /> <VtsLoadingHero v-if="!areBackupsReady" type="card" />
<VtsNoDataHero v-else-if="record?.backups === undefined" type="card" /> <VtsNoDataHero v-else-if="backups === undefined" type="card" />
<template v-else> <template v-else>
<VtsDonutChartWithLegend :segments="jobsSegments" :title="jobsTitle" /> <VtsDonutChartWithLegend :segments="jobsSegments" :title="jobsTitle" />
<UiCardNumbers :label="t('total')" :value="record.backups.jobs.total" size="small" /> <UiCardNumbers :label="t('total')" :value="backups.jobs.total" size="small" />
<VtsDivider type="stretch" /> <VtsDivider type="stretch" />
<VtsDonutChartWithLegend :segments="vmsProtectionSegments" :title="vmsProtectionTitle" /> <VtsDonutChartWithLegend :segments="vmsProtectionSegments" :title="vmsProtectionTitle" />
</template> </template>
@@ -13,7 +13,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useDashboardStore } from '@/stores/xo-rest-api/dashboard.store' import type { XoDashboard } from '@/types/xo/dashboard.type.ts'
import VtsDivider from '@core/components/divider/VtsDivider.vue' import VtsDivider from '@core/components/divider/VtsDivider.vue'
import VtsDonutChartWithLegend, { import VtsDonutChartWithLegend, {
type DonutChartWithLegendProps, type DonutChartWithLegendProps,
@@ -27,11 +27,11 @@ import { faCircleInfo } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { record } = useDashboardStore().subscribe() const { backups } = defineProps<{
backups: XoDashboard['backups'] | undefined
}>()
const areBackupsReady = computed( const areBackupsReady = computed(() => backups?.jobs !== undefined && backups?.vmsProtection !== undefined)
() => record.value?.backups?.jobs !== undefined && record.value?.backups?.vmsProtection !== undefined
)
const { t } = useI18n() const { t } = useI18n()
@@ -44,22 +44,22 @@ const jobsTitle = computed<DonutChartWithLegendProps['title']>(() => ({
const jobsSegments = computed<DonutChartWithLegendProps['segments']>(() => [ const jobsSegments = computed<DonutChartWithLegendProps['segments']>(() => [
{ {
label: t('backups.jobs.running-good'), label: t('backups.jobs.running-good'),
value: record.value?.backups?.jobs.successful ?? 0, value: backups?.jobs.successful ?? 0,
accent: 'success', accent: 'success',
}, },
{ {
label: t('backups.jobs.at-least-one-skipped'), label: t('backups.jobs.at-least-one-skipped'),
value: record.value?.backups?.jobs.skipped ?? 0, value: backups?.jobs.skipped ?? 0,
accent: 'info', accent: 'info',
}, },
{ {
label: t('backups.jobs.looks-like-issue'), label: t('backups.jobs.looks-like-issue'),
value: record.value?.backups?.jobs.failed ?? 0, value: backups?.jobs.failed ?? 0,
accent: 'danger', accent: 'danger',
}, },
{ {
label: t('backups.jobs.disabled'), label: t('backups.jobs.disabled'),
value: record.value?.backups?.jobs.disabled ?? 0, value: backups?.jobs.disabled ?? 0,
accent: 'muted', accent: 'muted',
}, },
]) ])
@@ -73,17 +73,17 @@ const vmsProtectionTitle = computed<DonutChartWithLegendProps['title']>(() => ({
const vmsProtectionSegments = computed<DonutChartWithLegendProps['segments']>(() => [ const vmsProtectionSegments = computed<DonutChartWithLegendProps['segments']>(() => [
{ {
label: t('backups.vms-protection.protected'), label: t('backups.vms-protection.protected'),
value: record.value?.backups?.vmsProtection.protected ?? 0, value: backups?.vmsProtection.protected ?? 0,
accent: 'success', accent: 'success',
}, },
{ {
label: t('backups.vms-protection.unprotected'), label: t('backups.vms-protection.unprotected'),
value: record.value?.backups?.vmsProtection.unprotected ?? 0, value: backups?.vmsProtection.unprotected ?? 0,
accent: 'warning', accent: 'warning',
}, },
{ {
label: t('backups.vms-protection.no-job'), label: t('backups.vms-protection.no-job'),
value: record.value?.backups?.vmsProtection.notInJob ?? 0, value: backups?.vmsProtection.notInJob ?? 0,
accent: 'danger', accent: 'danger',
}, },
]) ])

View File

@@ -4,13 +4,13 @@
<VtsLoadingHero v-if="!areHostsStatusReady" type="card" /> <VtsLoadingHero v-if="!areHostsStatusReady" type="card" />
<template v-else> <template v-else>
<VtsDonutChartWithLegend :icon="faServer" :segments /> <VtsDonutChartWithLegend :icon="faServer" :segments />
<UiCardNumbers :label="t('total')" :value="record?.hostsStatus?.total" class="total" size="small" /> <UiCardNumbers :label="t('total')" :value="status?.total" class="total" size="small" />
</template> </template>
</UiCard> </UiCard>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useDashboardStore } from '@/stores/xo-rest-api/dashboard.store' import type { XoDashboard } from '@/types/xo/dashboard.type.ts'
import type { DonutChartWithLegendProps } from '@core/components/donut-chart-with-legend/VtsDonutChartWithLegend.vue' import type { DonutChartWithLegendProps } from '@core/components/donut-chart-with-legend/VtsDonutChartWithLegend.vue'
import VtsDonutChartWithLegend from '@core/components/donut-chart-with-legend/VtsDonutChartWithLegend.vue' import VtsDonutChartWithLegend from '@core/components/donut-chart-with-legend/VtsDonutChartWithLegend.vue'
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue' import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
@@ -21,25 +21,28 @@ import { faServer } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { t } = useI18n() const { status } = defineProps<{
const { record } = useDashboardStore().subscribe() status: XoDashboard['hostsStatus'] | undefined
}>()
const areHostsStatusReady = computed(() => record.value?.hostsStatus !== undefined) const { t } = useI18n()
const areHostsStatusReady = computed(() => status !== undefined)
const segments = computed<DonutChartWithLegendProps['segments']>(() => [ const segments = computed<DonutChartWithLegendProps['segments']>(() => [
{ {
label: t('hosts-status.running'), label: t('hosts-status.running'),
value: record.value?.hostsStatus?.running ?? 0, value: status?.running ?? 0,
accent: 'success', accent: 'success',
}, },
{ {
label: t('hosts-status.halted'), label: t('hosts-status.halted'),
value: record.value?.hostsStatus?.halted ?? 0, value: status?.halted ?? 0,
accent: 'warning', accent: 'warning',
}, },
{ {
label: t('hosts-status.unknown'), label: t('hosts-status.unknown'),
value: record.value?.hostsStatus?.unknown ?? 0, value: status?.unknown ?? 0,
accent: 'muted', accent: 'muted',
tooltip: t('hosts-status.unknown.tooltip'), tooltip: t('hosts-status.unknown.tooltip'),
}, },

View File

@@ -11,7 +11,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useDashboardStore } from '@/stores/xo-rest-api/dashboard.store' import type { XoDashboard } from '@/types/xo/dashboard.type.ts'
import VtsDivider from '@core/components/divider/VtsDivider.vue' import VtsDivider from '@core/components/divider/VtsDivider.vue'
import VtsDonutChartWithLegend, { import VtsDonutChartWithLegend, {
type DonutChartWithLegendProps, type DonutChartWithLegendProps,
@@ -22,11 +22,21 @@ import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import { computed, type ComputedRef } from 'vue' import { computed, type ComputedRef } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const {
missingPatches,
nPools = 0,
nHosts = 0,
nHostsEol = 0,
} = defineProps<{
missingPatches: XoDashboard['missingPatches'] | undefined
nPools: XoDashboard['nPools'] | undefined
nHosts: XoDashboard['nHosts'] | undefined
nHostsEol: XoDashboard['nHostsEol'] | undefined
}>()
const { t } = useI18n() const { t } = useI18n()
const { record } = useDashboardStore().subscribe() const arePatchesReady = computed(() => missingPatches !== undefined)
const arePatchesReady = computed(() => record.value?.missingPatches !== undefined)
const poolsTitle: ComputedRef<DonutChartWithLegendProps['title']> = computed(() => ({ const poolsTitle: ComputedRef<DonutChartWithLegendProps['title']> = computed(() => ({
label: t('pools'), label: t('pools'),
@@ -34,10 +44,7 @@ const poolsTitle: ComputedRef<DonutChartWithLegendProps['title']> = computed(()
const poolsSegments: ComputedRef<DonutChartWithLegendProps['segments']> = computed(() => { const poolsSegments: ComputedRef<DonutChartWithLegendProps['segments']> = computed(() => {
// @TODO: See with Clémence if `hasAuthorization === false` // @TODO: See with Clémence if `hasAuthorization === false`
const nPoolsWithMissingPatches = record.value?.missingPatches?.hasAuthorization const nPoolsWithMissingPatches = missingPatches?.hasAuthorization ? missingPatches.nPoolsWithMissingPatches : 0
? record.value.missingPatches.nPoolsWithMissingPatches
: 0
const nPools = record.value?.nPools ?? 0
const nUpToDatePools = nPools - nPoolsWithMissingPatches const nUpToDatePools = nPools - nPoolsWithMissingPatches
@@ -53,13 +60,8 @@ const hostsTitle: ComputedRef<DonutChartWithLegendProps['title']> = computed(()
const hostsSegments = computed(() => { const hostsSegments = computed(() => {
// @TODO: See with Clémence if `hasAuthorization === false` // @TODO: See with Clémence if `hasAuthorization === false`
const nHostsWithMissingPatches = record.value?.missingPatches?.hasAuthorization const nHostsWithMissingPatches = missingPatches?.hasAuthorization ? missingPatches.nHostsWithMissingPatches : 0
? record.value.missingPatches.nHostsWithMissingPatches const nUpToDateHosts = nHosts - (nHostsWithMissingPatches + nHostsEol)
: 0
const nHostsEol = record.value?.nHostsEol
const nHosts = record.value?.nHosts
const nUpToDateHosts = (nHosts ?? 0) - (nHostsWithMissingPatches + (nHostsEol ?? 0))
const segments: DonutChartWithLegendProps['segments'] = [ const segments: DonutChartWithLegendProps['segments'] = [
{ value: nUpToDateHosts, accent: 'success', label: t('up-to-date') }, { value: nUpToDateHosts, accent: 'success', label: t('up-to-date') },

View File

@@ -10,7 +10,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useDashboardStore } from '@/stores/xo-rest-api/dashboard.store' import type { XoDashboard } from '@/types/xo/dashboard.type.ts'
import VtsDonutChartWithLegend, { import VtsDonutChartWithLegend, {
type DonutChartWithLegendProps, type DonutChartWithLegendProps,
} from '@core/components/donut-chart-with-legend/VtsDonutChartWithLegend.vue' } from '@core/components/donut-chart-with-legend/VtsDonutChartWithLegend.vue'
@@ -23,31 +23,31 @@ import { useSum } from '@vueuse/math'
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { status } = defineProps<{
status: XoDashboard['poolsStatus'] | undefined
}>()
const { t } = useI18n() const { t } = useI18n()
const { record } = useDashboardStore().subscribe() const arePoolsStatusReady = computed(() => status !== undefined)
const arePoolsStatusReady = computed(() => record.value?.poolsStatus !== undefined) const total = useSum(() => Object.values(status ?? {}))
const poolStatus = computed(() => record.value?.poolsStatus)
const total = useSum(() => Object.values(poolStatus.value ?? {}))
const segments = computed<DonutChartWithLegendProps['segments']>(() => [ const segments = computed<DonutChartWithLegendProps['segments']>(() => [
{ {
label: t('pools-status.connected'), label: t('pools-status.connected'),
value: poolStatus.value?.connected ?? 0, value: status?.connected ?? 0,
accent: 'success', accent: 'success',
}, },
{ {
label: t('pools-status.unreachable'), label: t('pools-status.unreachable'),
value: poolStatus.value?.unreachable ?? 0, value: status?.unreachable ?? 0,
accent: 'warning', accent: 'warning',
tooltip: t('pools-status.unreachable.tooltip'), tooltip: t('pools-status.unreachable.tooltip'),
}, },
{ {
label: t('pools-status.unknown'), label: t('pools-status.unknown'),
value: poolStatus.value?.unknown ?? 0, value: status?.unknown ?? 0,
accent: 'muted', accent: 'muted',
tooltip: t('pools-status.unknown.tooltip'), tooltip: t('pools-status.unknown.tooltip'),
}, },

View File

@@ -1,10 +1,10 @@
<template> <template>
<UiCard :horizontal="!uiStore.isMobile"> <UiCard :horizontal="!uiStore.isMobile">
<BackupRepository /> <BackupRepository :repositories="backupRepositories" />
<VtsDivider type="stretch" /> <VtsDivider type="stretch" />
<StorageRepository /> <StorageRepository :repositories="storageRepositories" />
<VtsDivider type="stretch" /> <VtsDivider type="stretch" />
<S3BackupRepository /> <S3BackupRepository :size="s3Size" />
</UiCard> </UiCard>
</template> </template>
@@ -12,9 +12,17 @@
import BackupRepository from '@/components/site/dashboard/BackupRepository.vue' import BackupRepository from '@/components/site/dashboard/BackupRepository.vue'
import S3BackupRepository from '@/components/site/dashboard/S3BackupRepository.vue' import S3BackupRepository from '@/components/site/dashboard/S3BackupRepository.vue'
import StorageRepository from '@/components/site/dashboard/StorageRepository.vue' import StorageRepository from '@/components/site/dashboard/StorageRepository.vue'
import type { BackupRepositories, StorageRepositories } from '@/requests/use-site-dashboard.request.ts'
import type { XoDashboard } from '@/types/xo/dashboard.type.ts'
import VtsDivider from '@core/components/divider/VtsDivider.vue' import VtsDivider from '@core/components/divider/VtsDivider.vue'
import UiCard from '@core/components/ui/card/UiCard.vue' import UiCard from '@core/components/ui/card/UiCard.vue'
import { useUiStore } from '@core/stores/ui.store' import { useUiStore } from '@core/stores/ui.store'
const { backupRepositories, storageRepositories } = defineProps<{
s3Size: NonNullable<XoDashboard['backupRepositories']>['s3']['size'] | undefined
backupRepositories: BackupRepositories | undefined
storageRepositories: StorageRepositories | undefined
}>()
const uiStore = useUiStore() const uiStore = useUiStore()
</script> </script>

View File

@@ -18,7 +18,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useDashboardStore } from '@/stores/xo-rest-api/dashboard.store' import type { XoDashboard } from '@/types/xo/dashboard.type.ts'
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue' import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue' import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardNumbers from '@core/components/ui/card-numbers/UiCardNumbers.vue' import UiCardNumbers from '@core/components/ui/card-numbers/UiCardNumbers.vue'
@@ -27,14 +27,16 @@ import { formatSizeRaw } from '@core/utils/size.util'
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { record } = useDashboardStore().subscribe() const { resources } = defineProps<{
resources: XoDashboard['resourcesOverview'] | undefined
}>()
const { t } = useI18n() const { t } = useI18n()
const areResourcesOverviewReady = computed(() => record.value?.resourcesOverview !== undefined) const areResourcesOverviewReady = computed(() => resources !== undefined)
const nCpus = computed(() => record.value?.resourcesOverview?.nCpus) const nCpus = computed(() => resources?.nCpus)
const memorySize = computed(() => formatSizeRaw(record.value?.resourcesOverview?.memorySize, 1)) const memorySize = computed(() => formatSizeRaw(resources?.memorySize, 1))
const srSize = computed(() => formatSizeRaw(record.value?.resourcesOverview?.srSize, 1)) const srSize = computed(() => formatSizeRaw(resources?.srSize, 1))
</script> </script>
<style lang="postcss" scoped> <style lang="postcss" scoped>

View File

@@ -10,7 +10,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useDashboardStore } from '@/stores/xo-rest-api/dashboard.store' import type { XoDashboard } from '@/types/xo/dashboard.type.ts'
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue' import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
import UiCardNumbers from '@core/components/ui/card-numbers/UiCardNumbers.vue' import UiCardNumbers from '@core/components/ui/card-numbers/UiCardNumbers.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue' import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
@@ -18,13 +18,15 @@ import { formatSizeRaw } from '@core/utils/size.util'
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { record } = useDashboardStore().subscribe() const { size } = defineProps<{
size: NonNullable<XoDashboard['backupRepositories']>['s3']['size'] | undefined
}>()
const { t } = useI18n() const { t } = useI18n()
const areS3BackupRepositoriesReady = computed(() => record.value?.backupRepositories?.s3 !== undefined) const areS3BackupRepositoriesReady = computed(() => size !== undefined)
const usedSize = computed(() => formatSizeRaw(record.value?.backupRepositories?.s3.size.backups, 1)) const usedSize = computed(() => formatSizeRaw(size?.backups, 1))
</script> </script>
<style scoped lang="postcss"> <style scoped lang="postcss">

View File

@@ -9,20 +9,20 @@
<VtsStackedBarWithLegend :max-value="maxValue" :segments /> <VtsStackedBarWithLegend :max-value="maxValue" :segments />
<div class="numbers"> <div class="numbers">
<UiCardNumbers <UiCardNumbers
:value="storageRepositories?.used?.value" :value="repositories?.used?.value"
:unit="storageRepositories?.used?.prefix" :unit="repositories?.used?.prefix"
:label="t('used')" :label="t('used')"
size="medium" size="medium"
/> />
<UiCardNumbers <UiCardNumbers
:value="storageRepositories?.available?.value" :value="repositories?.available?.value"
:unit="storageRepositories?.available?.prefix" :unit="repositories?.available?.prefix"
:label="t('available')" :label="t('available')"
size="medium" size="medium"
/> />
<UiCardNumbers <UiCardNumbers
:value="storageRepositories?.total?.value" :value="repositories?.total?.value"
:unit="storageRepositories?.total?.prefix" :unit="repositories?.total?.prefix"
:label="t('total')" :label="t('total')"
size="medium" size="medium"
/> />
@@ -32,7 +32,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useDashboardStore } from '@/stores/xo-rest-api/dashboard.store' import type { StorageRepositories } from '@/requests/use-site-dashboard.request.ts'
import VtsStackedBarWithLegend, { import VtsStackedBarWithLegend, {
type StackedBarWithLegendProps, type StackedBarWithLegendProps,
} from '@core/components/stacked-bar-with-legend/VtsStackedBarWithLegend.vue' } from '@core/components/stacked-bar-with-legend/VtsStackedBarWithLegend.vue'
@@ -42,30 +42,32 @@ import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import { computed, type ComputedRef } from 'vue' import { computed, type ComputedRef } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { repositories } = defineProps<{
repositories: StorageRepositories | undefined
}>()
const { t } = useI18n() const { t } = useI18n()
const { storageRepositories } = useDashboardStore().subscribe() const areStorageRepositoriesReady = computed(() => repositories !== undefined)
const areStorageRepositoriesReady = computed(() => storageRepositories.value !== undefined)
const segments: ComputedRef<StackedBarWithLegendProps['segments']> = computed(() => [ const segments: ComputedRef<StackedBarWithLegendProps['segments']> = computed(() => [
{ {
label: t('xo-replications'), label: t('xo-replications'),
value: storageRepositories.value?.replicated?.value ?? 0, value: repositories?.replicated?.value ?? 0,
accent: 'info', accent: 'info',
unit: storageRepositories.value?.replicated?.prefix, unit: repositories?.replicated?.prefix,
}, },
{ {
label: t('other'), label: t('other'),
value: storageRepositories.value?.other?.value ?? 0, value: repositories?.other?.value ?? 0,
accent: 'warning', accent: 'warning',
unit: storageRepositories.value?.other?.prefix, unit: repositories?.other?.prefix,
}, },
]) ])
const maxValue = computed(() => ({ const maxValue = computed(() => ({
value: storageRepositories.value?.total?.value, value: repositories?.total?.value,
unit: storageRepositories.value?.total?.prefix, unit: repositories?.total?.prefix,
})) }))
</script> </script>

View File

@@ -4,13 +4,13 @@
<VtsLoadingHero v-if="!areVmsStatusReady" type="card" /> <VtsLoadingHero v-if="!areVmsStatusReady" type="card" />
<template v-else> <template v-else>
<VtsDonutChartWithLegend :icon="faDesktop" :segments /> <VtsDonutChartWithLegend :icon="faDesktop" :segments />
<UiCardNumbers :label="t('total')" :value="record?.vmsStatus?.total" class="total" size="small" /> <UiCardNumbers :label="t('total')" :value="status?.total" class="total" size="small" />
</template> </template>
</UiCard> </UiCard>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useDashboardStore } from '@/stores/xo-rest-api/dashboard.store' import type { XoDashboard } from '@/types/xo/dashboard.type.ts'
import VtsDonutChartWithLegend, { import VtsDonutChartWithLegend, {
type DonutChartWithLegendProps, type DonutChartWithLegendProps,
} from '@core/components/donut-chart-with-legend/VtsDonutChartWithLegend.vue' } from '@core/components/donut-chart-with-legend/VtsDonutChartWithLegend.vue'
@@ -22,26 +22,29 @@ import { faDesktop } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { t } = useI18n() const { status } = defineProps<{
const { record } = useDashboardStore().subscribe() status: XoDashboard['vmsStatus'] | undefined
}>()
const areVmsStatusReady = computed(() => record.value?.vmsStatus !== undefined) const { t } = useI18n()
const areVmsStatusReady = computed(() => status !== undefined)
const segments = computed<DonutChartWithLegendProps['segments']>(() => [ const segments = computed<DonutChartWithLegendProps['segments']>(() => [
{ {
label: t('vms-status.running'), label: t('vms-status.running'),
value: record.value?.vmsStatus?.active ?? 0, value: status?.active ?? 0,
accent: 'success', accent: 'success',
}, },
{ {
label: t('vms-status.inactive'), label: t('vms-status.inactive'),
value: record.value?.vmsStatus?.inactive ?? 0, value: status?.inactive ?? 0,
accent: 'neutral', accent: 'neutral',
tooltip: t('vms-status.inactive.tooltip'), tooltip: t('vms-status.inactive.tooltip'),
}, },
{ {
label: t('vms-status.unknown'), label: t('vms-status.unknown'),
value: record.value?.vmsStatus?.unknown ?? 0, value: status?.unknown ?? 0,
accent: 'muted', accent: 'muted',
tooltip: t('vms-status.unknown.tooltip'), tooltip: t('vms-status.unknown.tooltip'),
}, },

View File

@@ -1,13 +1,24 @@
<template> <template>
<div class="site-dashboard" :class="{ mobile: uiStore.isMobile }"> <div class="site-dashboard" :class="{ mobile: uiStore.isMobile }">
<PoolsStatus class="pools-status" /> <PoolsStatus class="pools-status" :status="dashboard.poolsStatus" />
<HostsStatus class="hosts-status" /> <HostsStatus class="hosts-status" :status="dashboard.hostsStatus" />
<VmsStatus class="vms-status" /> <VmsStatus class="vms-status" :status="dashboard.vmsStatus" />
<ResourcesOverview class="resources-overview" /> <ResourcesOverview class="resources-overview" :resources="dashboard.resourcesOverview" />
<Backups class="backups" /> <Backups class="backups" :backups="dashboard.backups" />
<BackupIssues class="backup-issues" /> <BackupIssues class="backup-issues" :issues="dashboard.backups?.issues" />
<Repositories class="repositories" /> <Repositories
<Patches class="patches" /> class="repositories"
:backup-repositories
:storage-repositories
:s3-size="dashboard.backupRepositories?.s3?.size"
/>
<Patches
class="patches"
:missing-patches="dashboard.missingPatches"
:n-hosts="dashboard.nHosts"
:n-hosts-eol="dashboard.nHostsEol"
:n-pools="dashboard.nPools"
/>
</div> </div>
</template> </template>
@@ -20,9 +31,12 @@ import PoolsStatus from '@/components/site/dashboard/PoolsStatus.vue'
import Repositories from '@/components/site/dashboard/Repositories.vue' import Repositories from '@/components/site/dashboard/Repositories.vue'
import ResourcesOverview from '@/components/site/dashboard/ResourcesOverview.vue' import ResourcesOverview from '@/components/site/dashboard/ResourcesOverview.vue'
import VmsStatus from '@/components/site/dashboard/VmsStatus.vue' import VmsStatus from '@/components/site/dashboard/VmsStatus.vue'
import { useSiteDashboard } from '@/requests/use-site-dashboard.request.ts'
import { useUiStore } from '@core/stores/ui.store.ts' import { useUiStore } from '@core/stores/ui.store.ts'
const uiStore = useUiStore() const uiStore = useUiStore()
const { dashboard, backupRepositories, storageRepositories } = useSiteDashboard()
</script> </script>
<style lang="postcss" scoped> <style lang="postcss" scoped>

View File

@@ -0,0 +1,17 @@
import type { XoPoolDashboard } from '@/types/xo/pool-dashboard.type.ts'
import { defineRequest } from '@core/packages/request/define-request.ts'
import { merge } from 'lodash-es'
import { ref } from 'vue'
export const usePoolDashboard = defineRequest({
url: (poolId: string) => `/rest/v0/pools/${poolId}/dashboard?fields=*&ndjson=true`,
state: () => ({
dashboard: ref({} as XoPoolDashboard),
}),
onUrlChange: ({ dashboard }) => {
dashboard.value = {} as XoPoolDashboard
},
onDataReceived: ({ dashboard }, data) => {
merge(dashboard.value, data)
},
})

View File

@@ -0,0 +1,68 @@
import type { XoDashboard } from '@/types/xo/dashboard.type.ts'
import { defineRequest } from '@core/packages/request/define-request.ts'
import { formatSizeRaw } from '@core/utils/size.util.ts'
import type { Info, Scale } from 'human-format'
import { merge } from 'lodash-es'
import { computed, ref } from 'vue'
export const useSiteDashboard = defineRequest({
url: '/rest/v0/dashboard?fields=*&ndjson=true',
state: buildState,
onDataReceived: ({ dashboard }, data) => {
merge(dashboard.value, data)
},
})
export type BackupRepositories = {
available: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
backups: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
other: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
total: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
used: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
}
export type StorageRepositories = {
total: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
used: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
available: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
replicated: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
other: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
}
function buildState() {
const dashboard = ref({} as XoDashboard)
const backupRepositories = computed<BackupRepositories | undefined>(() => {
if (dashboard.value.backupRepositories === undefined) {
return
}
return {
available: formatSizeRaw(dashboard.value.backupRepositories.other.size.available, 1),
backups: formatSizeRaw(dashboard.value.backupRepositories.other.size.backups, 1),
other: formatSizeRaw(dashboard.value.backupRepositories.other.size.other ?? 0, 1),
total: formatSizeRaw(dashboard.value.backupRepositories.other.size.total, 1),
used: formatSizeRaw(dashboard.value.backupRepositories.other.size.used ?? 0, 1),
}
})
const storageRepositories = computed<StorageRepositories | undefined>(() => {
if (dashboard.value.storageRepositories === undefined) {
return
}
return {
total: formatSizeRaw(dashboard.value.storageRepositories.size.total, 1),
used: formatSizeRaw(dashboard.value.storageRepositories.size.used, 1),
available: formatSizeRaw(dashboard.value.storageRepositories.size.available, 1),
replicated: formatSizeRaw(dashboard.value.storageRepositories.size.replicated, 1),
other: formatSizeRaw(dashboard.value.storageRepositories.size.other, 1),
}
})
return {
dashboard,
backupRepositories,
storageRepositories,
}
}

View File

@@ -1,64 +0,0 @@
import { createXoStoreConfig } from '@/utils/create-xo-store-config.util'
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
import { formatSizeRaw } from '@core/utils/size.util'
import type { Info, Scale } from 'human-format'
import { defineStore } from 'pinia'
import { computed, type ComputedRef } from 'vue'
export const useDashboardStore = defineStore('dashboard', () => {
const { context: baseContext, ...configRest } = createXoStoreConfig('dashboard', { pollInterval: 5000 })
const backupRepositories: ComputedRef<
| undefined
| {
available: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
backups: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
other: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
total: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
used: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
}
> = computed(() => {
if (baseContext.record.value?.backupRepositories === undefined) {
return
}
return {
available: formatSizeRaw(baseContext.record.value.backupRepositories.other.size.available, 1),
backups: formatSizeRaw(baseContext.record.value.backupRepositories.other.size.backups, 1),
other: formatSizeRaw(baseContext.record.value.backupRepositories.other.size.other, 1),
total: formatSizeRaw(baseContext.record.value.backupRepositories.other.size.total, 1),
used: formatSizeRaw(baseContext.record.value.backupRepositories.other.size.used, 1),
}
})
const storageRepositories: ComputedRef<
| undefined
| {
total: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
used: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
available: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
replicated: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
other: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
}
> = computed(() => {
if (baseContext.record.value?.storageRepositories === undefined) {
return
}
return {
total: formatSizeRaw(baseContext.record.value.storageRepositories.size.total, 1),
used: formatSizeRaw(baseContext.record.value.storageRepositories.size.used, 1),
available: formatSizeRaw(baseContext.record.value.storageRepositories.size.available, 1),
replicated: formatSizeRaw(baseContext.record.value.storageRepositories.size.replicated, 1),
other: formatSizeRaw(baseContext.record.value.storageRepositories.size.other, 1),
}
})
const context = {
...baseContext,
backupRepositories,
storageRepositories,
}
return createSubscribableStoreContext({ context, ...configRest }, {})
})

View File

@@ -4,13 +4,9 @@ import { sortByNameLabel } from '@core/utils/sort-by-name-label.util'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
export const usePoolStore = defineStore('pool', () => { export const usePoolStore = defineStore('pool', () => {
const { context: baseContext, ...configRest } = createXoStoreConfig('pool', { const config = createXoStoreConfig('pool', {
sortBy: sortByNameLabel, sortBy: sortByNameLabel,
}) })
const context = { return createSubscribableStoreContext(config, {})
...baseContext,
}
return createSubscribableStoreContext({ context, ...configRest }, {})
}) })

View File

@@ -0,0 +1,5 @@
import type { Branded, XoAlarm as VatesXoAlarm } from '@vates/types'
export type XoAlarm = Pick<VatesXoAlarm, 'time' | 'body' | 'object'> & {
id: Branded<'ALARM'>
}

View File

@@ -27,9 +27,9 @@ export type XoDashboard = {
size: { size: {
available: number available: number
backups: number backups: number
other: number other?: number
total: number total: number
used: number used?: number
} }
} }
} }

View File

@@ -0,0 +1,54 @@
import type { XoAlarm } from '@/types/xo/alarm.type.ts'
import type { XoHost } from '@/types/xo/host.type.ts'
import type { XoSr } from '@/types/xo/sr.type.ts'
import type { XoVm } from '@/types/xo/vm.type.ts'
import type { XcpPatches, XsPatches } from '@vates/types'
export type XoPoolDashboard = {
hosts?: {
status?: {
running: number
disabled: number
halted: number
total: number
}
topFiveUsage?: {
ram: { name_label: string; size: number; usage: number; percent: number; id: XoHost['id'] }[]
cpu: { name_label: string; percent: number; id: XoHost['id'] }[]
}
missingPatches?:
| {
hasAuthorization: false
}
| { hasAuthorization: true; missingPatches: (XcpPatches | XsPatches)[] }
}
vms?: {
status?: {
running: number
halted: number
paused: number
total: number
suspended: number
}
topFiveUsage?: {
cpu: { id: XoVm['id']; name_label: string; percent: number }[]
ram: { id: XoVm['id']; name_label: string; percent: number; memory: number; memoryFree: number }[]
isExpired?: boolean
}
}
srs?: {
topFiveUsage?: {
name_label: string
id: XoSr['id']
percent: number
physical_usage: number
size: number
}[]
}
alarms?: XoAlarm['id'][]
cpuProvisioning?: {
total: number
assigned: number
percent: number
}
}

View File

@@ -15319,6 +15319,11 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
ndjson-readablestream@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/ndjson-readablestream/-/ndjson-readablestream-1.2.0.tgz#9c0929f272450a54f03a874662355ccab09e6e8c"
integrity sha512-QbWX2IIfKMVL+ZFHm9vFEzPh1NzZfzJql59T+9XoXzUp8n0wu2t9qgDV9nT0A77YYa6KbAjsHNWzJfpZTfp4xQ==
ndjson@^2.0.0: ndjson@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/ndjson/-/ndjson-2.0.0.tgz#320ac86f6fe53f5681897349b86ac6f43bfa3a19" resolved "https://registry.yarnpkg.com/ndjson/-/ndjson-2.0.0.tgz#320ac86f6fe53f5681897349b86ac6f43bfa3a19"