diff --git a/src/components/dashboard/Usage.vue b/src/components/dashboard/Usage.vue index 9705e364cc..d3bdb799b9 100644 --- a/src/components/dashboard/Usage.vue +++ b/src/components/dashboard/Usage.vue @@ -45,11 +45,9 @@ const props = defineProps<{ const plans = ref([]) const { t } = useI18n() -const noData = computed(() => false) const loadedAlready = ref(false) const storageUsageMode = ref<'total' | 'hourly'>('total') const storageUnit = computed(() => storageUsageMode.value === 'total' ? t('units-gb') : t('units-gb-hours')) -// const noData = computed(() => data.value.mau.length == 0) const data = ref({ mau: [] as number[], @@ -123,6 +121,11 @@ const main = useMainStore() const organizationStore = useOrganizationStore() const dashboardAppsStore = useDashboardAppsStore() const dialogStore = useDialogV2Store() + +// Demo charts stand in for the payment-failed state and for organizations with no apps yet. +const isDemoView = computed(() => props.forceDemo || (dashboardAppsStore.isLoaded && dashboardAppsStore.apps.length === 0)) +// No data to plot: loading is done, we are not showing demo charts, and every day is empty. +const noData = computed(() => !isLoading.value && !isDemoView.value && !data.value.mau.some(value => value !== undefined && value !== null)) const localOrgStatsUpdatedAt = ref(null) const localOrgStatsRefreshRequestedAt = ref(null) const localAppStatsUpdatedAt = ref(props.appStatsUpdatedAt ?? null) @@ -133,6 +136,9 @@ const autoRefreshScopeKey = ref(null) let refreshPollTimer: ReturnType | null = null let refreshPollStartedAt = 0 let refreshClockTimer: ReturnType | null = null +// Load the dashboard even if the store update never lands, so the spinner always clears. +let initialLoadFallbackTimer: ReturnType | null = null +const INITIAL_LOAD_FALLBACK_MS = 6000 const effectiveOrganization = computed(() => { if (props.appId) return organizationStore.getOrgByAppId(props.appId) ?? organizationStore.currentOrganization @@ -776,40 +782,47 @@ async function loadData() { const startTime = Date.now() isLoading.value = true - await getPlans().then((pls) => { - plans.value.length = 0 - plans.value.push(...pls) - }) + try { + await getPlans().then((pls) => { + plans.value.length = 0 + plans.value.push(...pls) + }) - // If forceDemo is true, use demo data instead of fetching real data - if (props.forceDemo) { - await loadDemoData() + // If forceDemo is true, use demo data instead of fetching real data + if (props.forceDemo) { + await loadDemoData() + } + else { + await getUsages(true) // Initial load - force fetch + } } - else { - await getUsages(true) // Initial load - force fetch + catch (error) { + // Let the charts fall back to their empty state instead of spinning forever. + console.error('Failed to load dashboard usage data:', error) } + finally { + // Ensure spinner shows for at least 300ms for better UX + const elapsed = Date.now() - startTime + if (elapsed < 300) { + await new Promise(resolve => setTimeout(resolve, 300 - elapsed)) + } + isLoading.value = false + chartsLoaded.value.usage = true + loadedAlready.value = true // Mark as loaded so watcher can reload data on mode changes + + // Stagger additional charts loading to improve perceived performance + setTimeout(() => { + chartsLoaded.value.bundles = true + }, 100) + + setTimeout(() => { + chartsLoaded.value.updates = true + }, 200) - // Ensure spinner shows for at least 300ms for better UX - const elapsed = Date.now() - startTime - if (elapsed < 300) { - await new Promise(resolve => setTimeout(resolve, 300 - elapsed)) + setTimeout(() => { + chartsLoaded.value.deployments = true + }, 300) } - isLoading.value = false - chartsLoaded.value.usage = true - loadedAlready.value = true // Mark as loaded so watcher can reload data on mode changes - - // Stagger additional charts loading to improve perceived performance - setTimeout(() => { - chartsLoaded.value.bundles = true - }, 100) - - setTimeout(() => { - chartsLoaded.value.updates = true - }, 200) - - setTimeout(() => { - chartsLoaded.value.deployments = true - }, 300) } // Watch for organization changes - show loading immediately when org switches @@ -964,8 +977,15 @@ onMounted(async () => { loadData() } } - // If dashboard not fetched yet, the watcher on 'dashboard' will handle loading - // and will check needsForceRefresh there + else { + // Dashboard not fetched yet: the watcher on 'dashboard' loads it once the store + // updates. If that fetch fails the watcher never fires, so load anyway after a + // bounded wait to clear the spinner. + initialLoadFallbackTimer = setTimeout(() => { + if (!loadedAlready.value) + loadData() + }, INITIAL_LOAD_FALLBACK_MS) + } }) onBeforeUnmount(() => { @@ -973,6 +993,10 @@ onBeforeUnmount(() => { clearInterval(refreshClockTimer) refreshClockTimer = null } + if (initialLoadFallbackTimer !== null) { + clearTimeout(initialLoadFallbackTimer) + initialLoadFallbackTimer = null + } stopRefreshPolling() }) @@ -1151,4 +1175,14 @@ onBeforeUnmount(() => { + +
+ +

+ {{ t('no-data') }} +

+