/** * The full dimension table (visitors, page views), in the Details window. The * AI row is pinned on the channel dimension even at low volume; when there is * no AI traffic yet, the table says what Saddle Analytics is watching for * instead of rendering an empty row that reads as broken. */ import { useEffect, useState } from 'react'; import { __ } from '@wordpress/i18n'; import { useUi } from './ui-context'; import { api } from './api'; import { AI_CHANNEL, pinAI } from './sort'; import LoadError from './load-error'; // Shared by the drawer table and the breakdown bar lists. export function labelFor( row, dimension, meta ) { if ( row.value === null || row.value === '' ) { return __( 'None', 'saddle-analytics' ); } if ( dimension.format === 'channel' ) { return meta.channels[ row.value ] ?? row.value; } if ( dimension.format === 'device' ) { return meta.devices[ row.value ] ?? row.value; } if ( dimension.key === 'country' ) { try { return new Intl.DisplayNames( undefined, { type: 'region' } ).of( String( row.value ) ) ?? String( row.value ); } catch { return String( row.value ); } } return String( row.value ); } const HEADERS = () => ( { visitors: __( 'Visitors', 'saddle-analytics' ), pageviews: __( 'Page views', 'saddle-analytics' ), } ); export default function ReportTable( { dimension, win, filters, meta, onFilter, stickyHeader = false, pageSize = 0, search = '', metric: initial = 'visitors' } ) { const { DataTable, Notice } = useUi(); // Starts on the card's metric; a column header re-sorts from there. const [ metric, setMetric ] = useState( initial ); const [ rows, setRows ] = useState( [] ); const [ loading, setLoading ] = useState( true ); const [ error, setError ] = useState( false ); const [ tick, setTick ] = useState( 0 ); useEffect( () => { if ( ! dimension ) { return; } let live = true; setLoading( true ); setError( false ); api( 'report', { dimension: dimension.key, metric, ...win, filters } ) .then( ( data ) => { if ( live ) { setRows( data.rows ); setLoading( false ); } } ) .catch( ( err ) => { if ( live ) { setLoading( false ); setError( err ); } } ); return () => { live = false; }; }, [ dimension?.key, metric, win?.range, win?.from, win?.to, JSON.stringify( filters ), tick ] ); if ( ! dimension ) { return null; } if ( error ) { return setTick( ( t ) => t + 1 ) } error={ error } />; } const label = ( row ) => labelFor( row, dimension, meta ); const headers = HEADERS(); // Search matches the display label, so "United States" finds country US rows. const query = search.trim().toLowerCase(); const visible = query ? rows.filter( ( row ) => String( label( row ) ).toLowerCase().includes( query ) ) : rows; const isChannel = dimension.key === 'channel'; const sorted = pinAI( visible, dimension.key ); const hasAI = rows.some( ( row ) => String( row.value ) === AI_CHANNEL ); const num = ( key, fmt ) => ( { key, header: headers[ key ], align: 'right', sortable: true, render: fmt, } ); return (
row.visitors.toLocaleString() ), num( 'pageviews', ( row ) => row.pageviews.toLocaleString() ), ] } rows={ sorted } rowId={ ( row ) => String( row.value ) } sort={ { key: metric, dir: 'desc' } } onSort={ ( key ) => key !== 'value' && setMetric( key ) } loading={ loading } stickyHeader={ stickyHeader } pageSize={ pageSize } onRowClick={ onFilter ? ( row ) => dimension.group !== 'pages' && onFilter( dimension.key, row.value, label( row ) ) : undefined } emptyTitle={ __( 'No data in this range yet', 'saddle-analytics' ) } /> { isChannel && ! hasAI && ! loading && '' === query && ( { __( 'No visits from AI assistants yet, such as chatgpt.com, claude.ai or perplexity.ai.', 'saddle-analytics' ) } ) }
); }