import React, { useState, useEffect, useRef, useCallback } from 'react' import * as storage from '../../util/storage' import TopStats from './top-stats' import { useTopStatsQuery } from './fetch-top-stats' import { useMainGraphQuery } from './fetch-main-graph' import { PlausibleSite, useSiteContext } from '../../site-context' import { Metric } from '../metrics' import { MainGraph, MainGraphContainer, useMainGraphWidth } from './main-graph' import { useGraphIntervalContext } from './graph-interval-context' import { useSetImportsIncluded } from './imports-included-context' import { useGetAnnotations } from '../../annotations/routeless-annotations-modals' // height of at least one row of top stats const DEFAULT_TOP_STATS_LOADING_HEIGHT_PX = 85 const DEFAULT_GRAPH_METRIC = 'visitors' export default function VisitorGraph({ updateImportedDataInView }: { updateImportedDataInView?: (v: boolean) => void }) { const topStatsBoundary = useRef(null) const mainGraphContainer = useRef(null) const { width } = useMainGraphWidth(mainGraphContainer) const site = useSiteContext() const getAnnotationsQuery = useGetAnnotations() const { selectedInterval } = useGraphIntervalContext() const [selectedMetric, setSelectedMetric] = useState( getStoredMetric(site) || DEFAULT_GRAPH_METRIC ) const onMetricClick = useCallback( (metric: Metric) => { setStoredMetric(site, metric) setSelectedMetric(metric) }, [site] ) const { apiState: topStatsApiState, isRealtimeSilentUpdate: isTopStatsRealtimeSilentUpdate } = useTopStatsQuery() const { apiState: mainGraphApiState, isRealtimeSilentUpdate: isMainGraphRealtimeSilentUpdate } = useMainGraphQuery(selectedMetric, selectedInterval) // Fall back to default graph metric if the stored metric // does not exist in the returned Top Stats useEffect(() => { if (topStatsApiState.data) { const availableMetrics = topStatsApiState.data.query.metrics setSelectedMetric((currentlySelectedMetric) => { if ( currentlySelectedMetric && availableMetrics.includes(currentlySelectedMetric) ) { return currentlySelectedMetric } else { return DEFAULT_GRAPH_METRIC } }) } }, [topStatsApiState.data]) // sync import related info useEffect(() => { if ( topStatsApiState.data && typeof updateImportedDataInView === 'function' ) { updateImportedDataInView( topStatsApiState.data.meta.imports_included as boolean ) } }, [topStatsApiState.data, updateImportedDataInView]) const switchVisible = !['no_imported_data', 'out_of_range'].includes( topStatsApiState.data?.meta.imports_skip_reason as string ) const switchDisabled = topStatsApiState.data?.meta.imports_skip_reason === 'unsupported_query' const setImportsIncluded = useSetImportsIncluded() useEffect(() => { if (topStatsApiState.data) { setImportsIncluded({ switchVisible, switchDisabled }) } else { setImportsIncluded(null) } }, [topStatsApiState.data, switchVisible, switchDisabled, setImportsIncluded]) useEffect(() => { return () => setImportsIncluded(null) }, [setImportsIncluded]) const { heightPx } = useGuessTopStatsHeight(site, topStatsBoundary) const showFullLoader = topStatsApiState.isFetching && topStatsApiState.isStale && !isTopStatsRealtimeSilentUpdate const showGraphLoader = mainGraphApiState.isFetching && mainGraphApiState.isStale && !isMainGraphRealtimeSilentUpdate && !showFullLoader return (
<>
{topStatsApiState.data ? ( ) : ( // prevent the top stats area from jumping on initial load
)}
{!!mainGraphApiState.data && !!width && ( <> {!showGraphLoader && ( )} {showGraphLoader && } )}
{(!(topStatsApiState.data && mainGraphApiState.data) || showFullLoader) && }
) } const Loader = () => { return (
) } function getStoredMetricKey(site: Pick) { return storage.getDomainScopedStorageKey('metric', site.domain) } function getStoredMetric(site: Pick) { return storage.getItem(getStoredMetricKey(site)) as Metric | null } function setStoredMetric(site: Pick, metric: Metric) { storage.setItem(getStoredMetricKey(site), metric) } function getStoredTopStatsHeightKey(site: Pick) { return storage.getDomainScopedStorageKey('topStatsHeight', site.domain) } function getStoredTopStatsHeight(site: Pick) { return storage.getItem(getStoredTopStatsHeightKey(site)) as string } function setStoredTopStatsHeight( site: Pick, heightPx: string ) { storage.setItem(getStoredTopStatsHeightKey(site), heightPx) } function useGuessTopStatsHeight( site: Pick, topStatsBoundary: React.RefObject ): { heightPx: string } { useEffect(() => { const resizeObserver = new ResizeObserver(() => { if (topStatsBoundary.current) { setStoredTopStatsHeight( site, `${Math.max(topStatsBoundary.current.clientHeight, DEFAULT_TOP_STATS_LOADING_HEIGHT_PX)}` ) } }) if (topStatsBoundary.current) { resizeObserver.observe(topStatsBoundary.current) } return () => { resizeObserver.disconnect() } }, [site, topStatsBoundary]) return { heightPx: getStoredTopStatsHeight(site) ?? DEFAULT_TOP_STATS_LOADING_HEIGHT_PX } }