mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-10 22:14:48 -05:00
feat(web-stack): introducing defineRequest + convert site dashboard (#8839)
This commit is contained in:
committed by
GitHub
parent
1045016545
commit
13f406fe70
@@ -171,7 +171,6 @@ module.exports = {
|
||||
'vue/no-empty-component-block': 'error',
|
||||
'vue/no-multiple-objects-in-class': 'error',
|
||||
'vue/no-ref-object-reactivity-loss': 'error',
|
||||
'vue/no-required-prop-with-default': 'error',
|
||||
'vue/no-static-inline-styles': 'error',
|
||||
'vue/no-template-target-blank': 'error',
|
||||
'vue/no-undef-components': ['error', { ignorePatterns: ['RouterLink', 'RouterView', 'I18nT'] }],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
"human-format": "^1.2.1",
|
||||
"iterable-backoff": "^0.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"ndjson-readablestream": "^1.2.0",
|
||||
"placement.js": "^1.0.0-beta.5",
|
||||
"simple-icons": "^14.14.0",
|
||||
"vue-echarts": "^6.6.8"
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"d3-time-format": "^4.1.0",
|
||||
"human-format": "^1.2.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"ndjson-readablestream": "^1.2.0",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"pinia": "^3.0.1",
|
||||
"postcss": "^8.5.3",
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</template>
|
||||
|
||||
<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 VtsDataTable from '@core/components/data-table/VtsDataTable.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 { useI18n } from 'vue-i18n'
|
||||
|
||||
const { issues } = defineProps<{
|
||||
issues: NonNullable<XoDashboard['backups']>['issues'] | undefined
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { record } = useDashboardStore().subscribe()
|
||||
const areBackupIssuesReady = computed(() => issues !== undefined)
|
||||
|
||||
const areBackupIssuesReady = computed(() => record.value?.backups?.issues !== undefined)
|
||||
|
||||
const backupIssues = computed(() => record.value?.backups?.issues ?? [])
|
||||
const backupIssues = computed(() => issues ?? [])
|
||||
|
||||
const logLabels = [t('last'), t('2nd-last'), t('3rd-last')]
|
||||
|
||||
|
||||
@@ -9,20 +9,20 @@
|
||||
<VtsStackedBarWithLegend :max-value="maxValue" :segments />
|
||||
<div class="numbers">
|
||||
<UiCardNumbers
|
||||
:value="backupRepositories?.used?.value"
|
||||
:unit="backupRepositories?.used?.prefix"
|
||||
:value="repositories?.used?.value"
|
||||
:unit="repositories?.used?.prefix"
|
||||
:label="t('used')"
|
||||
size="medium"
|
||||
/>
|
||||
<UiCardNumbers
|
||||
:value="backupRepositories?.available?.value"
|
||||
:unit="backupRepositories?.available?.prefix"
|
||||
:value="repositories?.available?.value"
|
||||
:unit="repositories?.available?.prefix"
|
||||
:label="t('available')"
|
||||
size="medium"
|
||||
/>
|
||||
<UiCardNumbers
|
||||
:value="backupRepositories?.total?.value"
|
||||
:unit="backupRepositories?.total?.prefix"
|
||||
:value="repositories?.total?.value"
|
||||
:unit="repositories?.total?.prefix"
|
||||
:label="t('total')"
|
||||
size="medium"
|
||||
/>
|
||||
@@ -32,7 +32,7 @@
|
||||
</template>
|
||||
|
||||
<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, {
|
||||
type StackedBarWithLegendProps,
|
||||
} 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 { useI18n } from 'vue-i18n'
|
||||
|
||||
const { repositories } = defineProps<{
|
||||
repositories: BackupRepositories | undefined
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { backupRepositories } = useDashboardStore().subscribe()
|
||||
|
||||
const areBackupRepositoriesReady = computed(() => backupRepositories.value !== undefined)
|
||||
const areBackupRepositoriesReady = computed(() => repositories !== undefined)
|
||||
|
||||
const segments: ComputedRef<StackedBarWithLegendProps['segments']> = computed(() => [
|
||||
{
|
||||
label: t('xo-backups'),
|
||||
value: backupRepositories.value?.backups?.value ?? 0,
|
||||
value: repositories?.backups.value ?? 0,
|
||||
accent: 'info',
|
||||
unit: backupRepositories.value?.backups?.prefix,
|
||||
unit: repositories?.backups.prefix,
|
||||
},
|
||||
{
|
||||
label: t('other'),
|
||||
value: backupRepositories.value?.other?.value ?? 0,
|
||||
value: repositories?.other?.value ?? 0,
|
||||
accent: 'warning',
|
||||
unit: backupRepositories.value?.other?.prefix,
|
||||
unit: repositories?.other?.prefix,
|
||||
},
|
||||
])
|
||||
|
||||
const maxValue = computed(() => ({
|
||||
value: backupRepositories.value?.total?.value,
|
||||
unit: backupRepositories.value?.total?.prefix,
|
||||
value: repositories?.total?.value,
|
||||
unit: repositories?.total?.prefix,
|
||||
}))
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<UiCard>
|
||||
<UiCardTitle>{{ t('backups') }}</UiCardTitle>
|
||||
<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>
|
||||
<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" />
|
||||
<VtsDonutChartWithLegend :segments="vmsProtectionSegments" :title="vmsProtectionTitle" />
|
||||
</template>
|
||||
@@ -13,7 +13,7 @@
|
||||
</template>
|
||||
|
||||
<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 VtsDonutChartWithLegend, {
|
||||
type DonutChartWithLegendProps,
|
||||
@@ -27,11 +27,11 @@ import { faCircleInfo } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { record } = useDashboardStore().subscribe()
|
||||
const { backups } = defineProps<{
|
||||
backups: XoDashboard['backups'] | undefined
|
||||
}>()
|
||||
|
||||
const areBackupsReady = computed(
|
||||
() => record.value?.backups?.jobs !== undefined && record.value?.backups?.vmsProtection !== undefined
|
||||
)
|
||||
const areBackupsReady = computed(() => backups?.jobs !== undefined && backups?.vmsProtection !== undefined)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -44,22 +44,22 @@ const jobsTitle = computed<DonutChartWithLegendProps['title']>(() => ({
|
||||
const jobsSegments = computed<DonutChartWithLegendProps['segments']>(() => [
|
||||
{
|
||||
label: t('backups.jobs.running-good'),
|
||||
value: record.value?.backups?.jobs.successful ?? 0,
|
||||
value: backups?.jobs.successful ?? 0,
|
||||
accent: 'success',
|
||||
},
|
||||
{
|
||||
label: t('backups.jobs.at-least-one-skipped'),
|
||||
value: record.value?.backups?.jobs.skipped ?? 0,
|
||||
value: backups?.jobs.skipped ?? 0,
|
||||
accent: 'info',
|
||||
},
|
||||
{
|
||||
label: t('backups.jobs.looks-like-issue'),
|
||||
value: record.value?.backups?.jobs.failed ?? 0,
|
||||
value: backups?.jobs.failed ?? 0,
|
||||
accent: 'danger',
|
||||
},
|
||||
{
|
||||
label: t('backups.jobs.disabled'),
|
||||
value: record.value?.backups?.jobs.disabled ?? 0,
|
||||
value: backups?.jobs.disabled ?? 0,
|
||||
accent: 'muted',
|
||||
},
|
||||
])
|
||||
@@ -73,17 +73,17 @@ const vmsProtectionTitle = computed<DonutChartWithLegendProps['title']>(() => ({
|
||||
const vmsProtectionSegments = computed<DonutChartWithLegendProps['segments']>(() => [
|
||||
{
|
||||
label: t('backups.vms-protection.protected'),
|
||||
value: record.value?.backups?.vmsProtection.protected ?? 0,
|
||||
value: backups?.vmsProtection.protected ?? 0,
|
||||
accent: 'success',
|
||||
},
|
||||
{
|
||||
label: t('backups.vms-protection.unprotected'),
|
||||
value: record.value?.backups?.vmsProtection.unprotected ?? 0,
|
||||
value: backups?.vmsProtection.unprotected ?? 0,
|
||||
accent: 'warning',
|
||||
},
|
||||
{
|
||||
label: t('backups.vms-protection.no-job'),
|
||||
value: record.value?.backups?.vmsProtection.notInJob ?? 0,
|
||||
value: backups?.vmsProtection.notInJob ?? 0,
|
||||
accent: 'danger',
|
||||
},
|
||||
])
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<VtsLoadingHero v-if="!areHostsStatusReady" type="card" />
|
||||
<template v-else>
|
||||
<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>
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<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 VtsDonutChartWithLegend from '@core/components/donut-chart-with-legend/VtsDonutChartWithLegend.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 { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { record } = useDashboardStore().subscribe()
|
||||
const { status } = defineProps<{
|
||||
status: XoDashboard['hostsStatus'] | undefined
|
||||
}>()
|
||||
|
||||
const areHostsStatusReady = computed(() => record.value?.hostsStatus !== undefined)
|
||||
const { t } = useI18n()
|
||||
|
||||
const areHostsStatusReady = computed(() => status !== undefined)
|
||||
|
||||
const segments = computed<DonutChartWithLegendProps['segments']>(() => [
|
||||
{
|
||||
label: t('hosts-status.running'),
|
||||
value: record.value?.hostsStatus?.running ?? 0,
|
||||
value: status?.running ?? 0,
|
||||
accent: 'success',
|
||||
},
|
||||
{
|
||||
label: t('hosts-status.halted'),
|
||||
value: record.value?.hostsStatus?.halted ?? 0,
|
||||
value: status?.halted ?? 0,
|
||||
accent: 'warning',
|
||||
},
|
||||
{
|
||||
label: t('hosts-status.unknown'),
|
||||
value: record.value?.hostsStatus?.unknown ?? 0,
|
||||
value: status?.unknown ?? 0,
|
||||
accent: 'muted',
|
||||
tooltip: t('hosts-status.unknown.tooltip'),
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</template>
|
||||
|
||||
<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 VtsDonutChartWithLegend, {
|
||||
type DonutChartWithLegendProps,
|
||||
@@ -22,11 +22,21 @@ import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
|
||||
import { computed, type ComputedRef } from 'vue'
|
||||
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 { record } = useDashboardStore().subscribe()
|
||||
|
||||
const arePatchesReady = computed(() => record.value?.missingPatches !== undefined)
|
||||
const arePatchesReady = computed(() => missingPatches !== undefined)
|
||||
|
||||
const poolsTitle: ComputedRef<DonutChartWithLegendProps['title']> = computed(() => ({
|
||||
label: t('pools'),
|
||||
@@ -34,10 +44,7 @@ const poolsTitle: ComputedRef<DonutChartWithLegendProps['title']> = computed(()
|
||||
|
||||
const poolsSegments: ComputedRef<DonutChartWithLegendProps['segments']> = computed(() => {
|
||||
// @TODO: See with Clémence if `hasAuthorization === false`
|
||||
const nPoolsWithMissingPatches = record.value?.missingPatches?.hasAuthorization
|
||||
? record.value.missingPatches.nPoolsWithMissingPatches
|
||||
: 0
|
||||
const nPools = record.value?.nPools ?? 0
|
||||
const nPoolsWithMissingPatches = missingPatches?.hasAuthorization ? missingPatches.nPoolsWithMissingPatches : 0
|
||||
|
||||
const nUpToDatePools = nPools - nPoolsWithMissingPatches
|
||||
|
||||
@@ -53,13 +60,8 @@ const hostsTitle: ComputedRef<DonutChartWithLegendProps['title']> = computed(()
|
||||
|
||||
const hostsSegments = computed(() => {
|
||||
// @TODO: See with Clémence if `hasAuthorization === false`
|
||||
const nHostsWithMissingPatches = record.value?.missingPatches?.hasAuthorization
|
||||
? record.value.missingPatches.nHostsWithMissingPatches
|
||||
: 0
|
||||
const nHostsEol = record.value?.nHostsEol
|
||||
const nHosts = record.value?.nHosts
|
||||
|
||||
const nUpToDateHosts = (nHosts ?? 0) - (nHostsWithMissingPatches + (nHostsEol ?? 0))
|
||||
const nHostsWithMissingPatches = missingPatches?.hasAuthorization ? missingPatches.nHostsWithMissingPatches : 0
|
||||
const nUpToDateHosts = nHosts - (nHostsWithMissingPatches + nHostsEol)
|
||||
|
||||
const segments: DonutChartWithLegendProps['segments'] = [
|
||||
{ value: nUpToDateHosts, accent: 'success', label: t('up-to-date') },
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useDashboardStore } from '@/stores/xo-rest-api/dashboard.store'
|
||||
import type { XoDashboard } from '@/types/xo/dashboard.type.ts'
|
||||
import VtsDonutChartWithLegend, {
|
||||
type DonutChartWithLegendProps,
|
||||
} from '@core/components/donut-chart-with-legend/VtsDonutChartWithLegend.vue'
|
||||
@@ -23,31 +23,31 @@ import { useSum } from '@vueuse/math'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { status } = defineProps<{
|
||||
status: XoDashboard['poolsStatus'] | undefined
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { record } = useDashboardStore().subscribe()
|
||||
const arePoolsStatusReady = computed(() => status !== undefined)
|
||||
|
||||
const arePoolsStatusReady = computed(() => record.value?.poolsStatus !== undefined)
|
||||
|
||||
const poolStatus = computed(() => record.value?.poolsStatus)
|
||||
|
||||
const total = useSum(() => Object.values(poolStatus.value ?? {}))
|
||||
const total = useSum(() => Object.values(status ?? {}))
|
||||
|
||||
const segments = computed<DonutChartWithLegendProps['segments']>(() => [
|
||||
{
|
||||
label: t('pools-status.connected'),
|
||||
value: poolStatus.value?.connected ?? 0,
|
||||
value: status?.connected ?? 0,
|
||||
accent: 'success',
|
||||
},
|
||||
{
|
||||
label: t('pools-status.unreachable'),
|
||||
value: poolStatus.value?.unreachable ?? 0,
|
||||
value: status?.unreachable ?? 0,
|
||||
accent: 'warning',
|
||||
tooltip: t('pools-status.unreachable.tooltip'),
|
||||
},
|
||||
{
|
||||
label: t('pools-status.unknown'),
|
||||
value: poolStatus.value?.unknown ?? 0,
|
||||
value: status?.unknown ?? 0,
|
||||
accent: 'muted',
|
||||
tooltip: t('pools-status.unknown.tooltip'),
|
||||
},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<UiCard :horizontal="!uiStore.isMobile">
|
||||
<BackupRepository />
|
||||
<BackupRepository :repositories="backupRepositories" />
|
||||
<VtsDivider type="stretch" />
|
||||
<StorageRepository />
|
||||
<StorageRepository :repositories="storageRepositories" />
|
||||
<VtsDivider type="stretch" />
|
||||
<S3BackupRepository />
|
||||
<S3BackupRepository :size="s3Size" />
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
@@ -12,9 +12,17 @@
|
||||
import BackupRepository from '@/components/site/dashboard/BackupRepository.vue'
|
||||
import S3BackupRepository from '@/components/site/dashboard/S3BackupRepository.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 UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
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()
|
||||
</script>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</template>
|
||||
|
||||
<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 UiCard from '@core/components/ui/card/UiCard.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 { useI18n } from 'vue-i18n'
|
||||
|
||||
const { record } = useDashboardStore().subscribe()
|
||||
const { resources } = defineProps<{
|
||||
resources: XoDashboard['resourcesOverview'] | undefined
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const areResourcesOverviewReady = computed(() => record.value?.resourcesOverview !== undefined)
|
||||
const nCpus = computed(() => record.value?.resourcesOverview?.nCpus)
|
||||
const memorySize = computed(() => formatSizeRaw(record.value?.resourcesOverview?.memorySize, 1))
|
||||
const srSize = computed(() => formatSizeRaw(record.value?.resourcesOverview?.srSize, 1))
|
||||
const areResourcesOverviewReady = computed(() => resources !== undefined)
|
||||
const nCpus = computed(() => resources?.nCpus)
|
||||
const memorySize = computed(() => formatSizeRaw(resources?.memorySize, 1))
|
||||
const srSize = computed(() => formatSizeRaw(resources?.srSize, 1))
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<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 UiCardNumbers from '@core/components/ui/card-numbers/UiCardNumbers.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 { useI18n } from 'vue-i18n'
|
||||
|
||||
const { record } = useDashboardStore().subscribe()
|
||||
const { size } = defineProps<{
|
||||
size: NonNullable<XoDashboard['backupRepositories']>['s3']['size'] | undefined
|
||||
}>()
|
||||
|
||||
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>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
|
||||
@@ -9,20 +9,20 @@
|
||||
<VtsStackedBarWithLegend :max-value="maxValue" :segments />
|
||||
<div class="numbers">
|
||||
<UiCardNumbers
|
||||
:value="storageRepositories?.used?.value"
|
||||
:unit="storageRepositories?.used?.prefix"
|
||||
:value="repositories?.used?.value"
|
||||
:unit="repositories?.used?.prefix"
|
||||
:label="t('used')"
|
||||
size="medium"
|
||||
/>
|
||||
<UiCardNumbers
|
||||
:value="storageRepositories?.available?.value"
|
||||
:unit="storageRepositories?.available?.prefix"
|
||||
:value="repositories?.available?.value"
|
||||
:unit="repositories?.available?.prefix"
|
||||
:label="t('available')"
|
||||
size="medium"
|
||||
/>
|
||||
<UiCardNumbers
|
||||
:value="storageRepositories?.total?.value"
|
||||
:unit="storageRepositories?.total?.prefix"
|
||||
:value="repositories?.total?.value"
|
||||
:unit="repositories?.total?.prefix"
|
||||
:label="t('total')"
|
||||
size="medium"
|
||||
/>
|
||||
@@ -32,7 +32,7 @@
|
||||
</template>
|
||||
|
||||
<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, {
|
||||
type StackedBarWithLegendProps,
|
||||
} 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 { useI18n } from 'vue-i18n'
|
||||
|
||||
const { repositories } = defineProps<{
|
||||
repositories: StorageRepositories | undefined
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { storageRepositories } = useDashboardStore().subscribe()
|
||||
|
||||
const areStorageRepositoriesReady = computed(() => storageRepositories.value !== undefined)
|
||||
const areStorageRepositoriesReady = computed(() => repositories !== undefined)
|
||||
|
||||
const segments: ComputedRef<StackedBarWithLegendProps['segments']> = computed(() => [
|
||||
{
|
||||
label: t('xo-replications'),
|
||||
value: storageRepositories.value?.replicated?.value ?? 0,
|
||||
value: repositories?.replicated?.value ?? 0,
|
||||
accent: 'info',
|
||||
unit: storageRepositories.value?.replicated?.prefix,
|
||||
unit: repositories?.replicated?.prefix,
|
||||
},
|
||||
{
|
||||
label: t('other'),
|
||||
value: storageRepositories.value?.other?.value ?? 0,
|
||||
value: repositories?.other?.value ?? 0,
|
||||
accent: 'warning',
|
||||
unit: storageRepositories.value?.other?.prefix,
|
||||
unit: repositories?.other?.prefix,
|
||||
},
|
||||
])
|
||||
|
||||
const maxValue = computed(() => ({
|
||||
value: storageRepositories.value?.total?.value,
|
||||
unit: storageRepositories.value?.total?.prefix,
|
||||
value: repositories?.total?.value,
|
||||
unit: repositories?.total?.prefix,
|
||||
}))
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<VtsLoadingHero v-if="!areVmsStatusReady" type="card" />
|
||||
<template v-else>
|
||||
<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>
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useDashboardStore } from '@/stores/xo-rest-api/dashboard.store'
|
||||
import type { XoDashboard } from '@/types/xo/dashboard.type.ts'
|
||||
import VtsDonutChartWithLegend, {
|
||||
type DonutChartWithLegendProps,
|
||||
} 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 { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { record } = useDashboardStore().subscribe()
|
||||
const { status } = defineProps<{
|
||||
status: XoDashboard['vmsStatus'] | undefined
|
||||
}>()
|
||||
|
||||
const areVmsStatusReady = computed(() => record.value?.vmsStatus !== undefined)
|
||||
const { t } = useI18n()
|
||||
|
||||
const areVmsStatusReady = computed(() => status !== undefined)
|
||||
|
||||
const segments = computed<DonutChartWithLegendProps['segments']>(() => [
|
||||
{
|
||||
label: t('vms-status.running'),
|
||||
value: record.value?.vmsStatus?.active ?? 0,
|
||||
value: status?.active ?? 0,
|
||||
accent: 'success',
|
||||
},
|
||||
{
|
||||
label: t('vms-status.inactive'),
|
||||
value: record.value?.vmsStatus?.inactive ?? 0,
|
||||
value: status?.inactive ?? 0,
|
||||
accent: 'neutral',
|
||||
tooltip: t('vms-status.inactive.tooltip'),
|
||||
},
|
||||
{
|
||||
label: t('vms-status.unknown'),
|
||||
value: record.value?.vmsStatus?.unknown ?? 0,
|
||||
value: status?.unknown ?? 0,
|
||||
accent: 'muted',
|
||||
tooltip: t('vms-status.unknown.tooltip'),
|
||||
},
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
<template>
|
||||
<div class="site-dashboard" :class="{ mobile: uiStore.isMobile }">
|
||||
<PoolsStatus class="pools-status" />
|
||||
<HostsStatus class="hosts-status" />
|
||||
<VmsStatus class="vms-status" />
|
||||
<ResourcesOverview class="resources-overview" />
|
||||
<Backups class="backups" />
|
||||
<BackupIssues class="backup-issues" />
|
||||
<Repositories class="repositories" />
|
||||
<Patches class="patches" />
|
||||
<PoolsStatus class="pools-status" :status="dashboard.poolsStatus" />
|
||||
<HostsStatus class="hosts-status" :status="dashboard.hostsStatus" />
|
||||
<VmsStatus class="vms-status" :status="dashboard.vmsStatus" />
|
||||
<ResourcesOverview class="resources-overview" :resources="dashboard.resourcesOverview" />
|
||||
<Backups class="backups" :backups="dashboard.backups" />
|
||||
<BackupIssues class="backup-issues" :issues="dashboard.backups?.issues" />
|
||||
<Repositories
|
||||
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>
|
||||
</template>
|
||||
|
||||
@@ -20,9 +31,12 @@ import PoolsStatus from '@/components/site/dashboard/PoolsStatus.vue'
|
||||
import Repositories from '@/components/site/dashboard/Repositories.vue'
|
||||
import ResourcesOverview from '@/components/site/dashboard/ResourcesOverview.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'
|
||||
|
||||
const uiStore = useUiStore()
|
||||
|
||||
const { dashboard, backupRepositories, storageRepositories } = useSiteDashboard()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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 }, {})
|
||||
})
|
||||
@@ -4,13 +4,9 @@ import { sortByNameLabel } from '@core/utils/sort-by-name-label.util'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const usePoolStore = defineStore('pool', () => {
|
||||
const { context: baseContext, ...configRest } = createXoStoreConfig('pool', {
|
||||
const config = createXoStoreConfig('pool', {
|
||||
sortBy: sortByNameLabel,
|
||||
})
|
||||
|
||||
const context = {
|
||||
...baseContext,
|
||||
}
|
||||
|
||||
return createSubscribableStoreContext({ context, ...configRest }, {})
|
||||
return createSubscribableStoreContext(config, {})
|
||||
})
|
||||
|
||||
5
@xen-orchestra/web/src/types/xo/alarm.type.ts
Normal file
5
@xen-orchestra/web/src/types/xo/alarm.type.ts
Normal 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'>
|
||||
}
|
||||
@@ -27,9 +27,9 @@ export type XoDashboard = {
|
||||
size: {
|
||||
available: number
|
||||
backups: number
|
||||
other: number
|
||||
other?: number
|
||||
total: number
|
||||
used: number
|
||||
used?: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
54
@xen-orchestra/web/src/types/xo/pool-dashboard.type.ts
Normal file
54
@xen-orchestra/web/src/types/xo/pool-dashboard.type.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -15319,6 +15319,11 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
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:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ndjson/-/ndjson-2.0.0.tgz#320ac86f6fe53f5681897349b86ac6f43bfa3a19"
|
||||
|
||||
Reference in New Issue
Block a user