import { FullPageEmptyState } from '@/components/full-page-empty-state'; import { OverviewInterval } from '@/components/overview/overview-interval'; import { OverviewRange } from '@/components/overview/overview-range'; import { useOverviewOptions } from '@/components/overview/useOverviewOptions'; import { PageContainer } from '@/components/page-container'; import { PageHeader } from '@/components/page-header'; import { FloatingPagination } from '@/components/pagination-floating'; import { ReportChart } from '@/components/report-chart'; import { Skeleton } from '@/components/skeleton'; import { Input } from '@/components/ui/input'; import { TableButtons } from '@/components/ui/table'; import { useAppContext } from '@/hooks/use-app-context'; import { useNumber } from '@/hooks/use-numer-formatter'; import { useSearchQueryState } from '@/hooks/use-search-query-state'; import { useTRPC } from '@/integrations/trpc/react'; import type { RouterOutputs } from '@/trpc/client'; import { PAGE_TITLES, createProjectTitle } from '@/utils/title'; import type { IChartRange, IInterval } from '@openpanel/validation'; import { keepPreviousData, useQuery } from '@tanstack/react-query'; import { createFileRoute } from '@tanstack/react-router'; import { parseAsInteger, useQueryState } from 'nuqs'; import { memo, useEffect, useMemo, useState } from 'react'; export const Route = createFileRoute('/_app/$organizationId/$projectId/pages')({ component: Component, head: () => { return { meta: [ { title: createProjectTitle(PAGE_TITLES.PAGES), }, ], }; }, }); function Component() { const { projectId } = Route.useParams(); const trpc = useTRPC(); const take = 20; const { range, interval } = useOverviewOptions(); const [cursor, setCursor] = useQueryState( 'cursor', parseAsInteger.withDefault(1), ); const { debouncedSearch, setSearch, search } = useSearchQueryState(); // Track if we should use backend search (when client-side filtering finds nothing) const [useBackendSearch, setUseBackendSearch] = useState(false); // Reset to client-side filtering when search changes useEffect(() => { setUseBackendSearch(false); setCursor(1); }, [debouncedSearch, setCursor]); // Query for all pages (without search) - used for client-side filtering const allPagesQuery = useQuery( trpc.event.pages.queryOptions( { projectId, cursor: 1, take: 1000, search: undefined, // No search - get all pages range, interval, }, { placeholderData: keepPreviousData, }, ), ); // Query for backend search (only when client-side filtering finds nothing) const backendSearchQuery = useQuery( trpc.event.pages.queryOptions( { projectId, cursor: 1, take: 1000, search: debouncedSearch || undefined, range, interval, }, { placeholderData: keepPreviousData, enabled: useBackendSearch && !!debouncedSearch, }, ), ); // Client-side filtering: filter all pages by search query const clientSideFiltered = useMemo(() => { if (!debouncedSearch || useBackendSearch) { return allPagesQuery.data ?? []; } const searchLower = debouncedSearch.toLowerCase(); return (allPagesQuery.data ?? []).filter( (page) => page.path.toLowerCase().includes(searchLower) || page.origin.toLowerCase().includes(searchLower), ); }, [allPagesQuery.data, debouncedSearch, useBackendSearch]); // Check if client-side filtering found results useEffect(() => { if ( debouncedSearch && !useBackendSearch && allPagesQuery.isSuccess && clientSideFiltered.length === 0 ) { // No results from client-side filtering, switch to backend search setUseBackendSearch(true); } }, [ debouncedSearch, useBackendSearch, allPagesQuery.isSuccess, clientSideFiltered.length, ]); // Determine which data source to use const allData = useBackendSearch ? (backendSearchQuery.data ?? []) : clientSideFiltered; const isLoading = useBackendSearch ? backendSearchQuery.isLoading : allPagesQuery.isLoading; // Client-side pagination: slice the items based on cursor const startIndex = (cursor - 1) * take; const endIndex = startIndex + take; const data = allData.slice(startIndex, endIndex); const totalPages = Math.ceil(allData.length / take); return ( { setSearch(e.target.value); setCursor(1); }} /> {data.length === 0 && !isLoading && ( )} {isLoading && (
)}
{data.map((page) => { return ( ); })}
{allData.length !== 0 && (
1 ? () => setCursor(1) : undefined} canNextPage={cursor < totalPages} canPreviousPage={cursor > 1} pageIndex={cursor - 1} nextPage={() => { setCursor((p) => Math.min(p + 1, totalPages)); }} previousPage={() => { setCursor((p) => Math.max(p - 1, 1)); }} />
)}
); } const PageCard = memo( ({ page, range, interval, projectId, }: { page: RouterOutputs['event']['pages'][number]; range: IChartRange; interval: IInterval; projectId: string; }) => { const number = useNumber(); const { apiUrl } = useAppContext(); return (
{page.title}
{page.title}
{page.path}
{number.formatWithUnit(page.avg_duration, 'min')}
duration
{number.formatWithUnit(page.bounce_rate / 100, '%')}
bounce rate
{number.format(page.sessions)}
sessions
); }, ); const PageCardSkeleton = memo(() => { return (
); });