diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/pages-table.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/pages-table.tsx deleted file mode 100644 index 08891283..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/pages-table.tsx +++ /dev/null @@ -1,128 +0,0 @@ -'use client'; - -import { ReportChart } from '@/components/report-chart'; -import { useNumber } from '@/hooks/useNumerFormatter'; -import { cn } from '@/utils/cn'; -import isEqual from 'lodash.isequal'; -import { ExternalLinkIcon } from 'lucide-react'; -import { memo } from 'react'; - -import type { IServicePage } from '@openpanel/db'; - -export const PagesTable = memo( - ({ data }: { data: IServicePage[] }) => { - const number = useNumber(); - const cell = - 'flex min-h-12 whitespace-nowrap px-4 align-middle shadow-[0_0_0_0.5px] shadow-border'; - return ( -
-
-
-
- Views -
-
- Path -
-
- Chart -
-
- {data.map((item, index) => { - return ( -
-
- {number.short(item.count)} -
-
- {item.title} - {item.origin ? ( - - - {item.path} - - ) : ( - - {item.path} - - )} -
-
- -
-
- ); - })} -
-
- ); - }, - (prevProps, nextProps) => { - return isEqual(prevProps.data, nextProps.data); - }, -); - -PagesTable.displayName = 'PagesTable'; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/pages.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/pages.tsx index 3494552e..3195af0f 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/pages.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/pages.tsx @@ -1,18 +1,26 @@ 'use client'; import { TableButtons } from '@/components/data-table'; -import { Pagination } from '@/components/pagination'; import { Input } from '@/components/ui/input'; -import { TableSkeleton } from '@/components/ui/table'; import { useDebounceValue } from '@/hooks/useDebounceValue'; -import { api } from '@/trpc/client'; -import { Loader2Icon } from 'lucide-react'; +import { type RouterOutputs, api } from '@/trpc/client'; import { parseAsInteger, useQueryState } from 'nuqs'; -import { PagesTable } from './pages-table'; +import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer'; +import { OverviewInterval } from '@/components/overview/overview-interval'; +import { OverviewRange } from '@/components/overview/overview-range'; +import { useOverviewOptions } from '@/components/overview/useOverviewOptions'; +import { Pagination } from '@/components/pagination'; +import { ReportChart } from '@/components/report-chart'; +import { useEventQueryFilters } from '@/hooks/useEventQueryFilters'; +import { useNumber } from '@/hooks/useNumerFormatter'; +import type { IChartRange, IInterval } from '@openpanel/validation'; +import { memo } from 'react'; export function Pages({ projectId }: { projectId: string }) { const take = 20; + const { range, interval } = useOverviewOptions(); + const [filters, setFilters] = useEventQueryFilters(); const [cursor, setCursor] = useQueryState( 'cursor', parseAsInteger.withDefault(0), @@ -28,6 +36,9 @@ export function Pages({ projectId }: { projectId: string }) { cursor, take, search: debouncedSearch, + range, + interval, + filters, }, { keepPreviousData: true, @@ -38,7 +49,11 @@ export function Pages({ projectId }: { projectId: string }) { return ( <> + + + { @@ -47,19 +62,132 @@ export function Pages({ projectId }: { projectId: string }) { }} /> - {query.isLoading ? ( - - ) : ( - - )} - +
+ {data.map((page) => { + return ( + + ); + })} +
+
+ +
); } + +const PageCard = memo( + ({ + page, + range, + interval, + projectId, + }: { + page: RouterOutputs['event']['pages'][number]; + range: IChartRange; + interval: IInterval; + projectId: string; + }) => { + const number = useNumber(); + return ( +
+
+
+
+ {page.title} +
+ + {page.path} + +
+
+
+
+
+ {number.formatWithUnit(page.avg_duration, 'min')} +
+
+ duration +
+
+
+
+ {number.formatWithUnit(page.bounce_rate / 100, '%')} +
+
+ bounce rate +
+
+
+
+ {number.format(page.sessions)} +
+
+ sessions +
+
+
+ +
+ ); + }, +); diff --git a/packages/db/src/services/event.service.ts b/packages/db/src/services/event.service.ts index 5a42f57f..bb6193fd 100644 --- a/packages/db/src/services/event.service.ts +++ b/packages/db/src/services/event.service.ts @@ -617,7 +617,7 @@ export async function getTopPages({ search?: string; }) { const res = await chQuery(` - SELECT path, count(*) as count, project_id, first_value(created_at) as first_seen, max(properties['__title']) as title, origin + SELECT path, count(*) as count, project_id, first_value(created_at) as first_seen, last_value(properties['__title']) as title, origin FROM ${TABLE_NAMES.events} WHERE name = 'screen_view' AND project_id = ${escape(projectId)} diff --git a/packages/db/src/services/overview.service.ts b/packages/db/src/services/overview.service.ts index 9b521850..3907f0d9 100644 --- a/packages/db/src/services/overview.service.ts +++ b/packages/db/src/services/overview.service.ts @@ -389,6 +389,7 @@ export class OverviewService { .select([ 'origin', 'path', + `last_value(properties['__title']) as title`, 'uniq(session_id) as count', 'round(avg(duration)/1000, 2) as avg_duration', ]) @@ -427,12 +428,14 @@ export class OverviewService { .with('page_stats', pageStatsQuery) .with('bounce_stats', bounceStatsQuery) .select<{ + title: string; origin: string; path: string; avg_duration: number; bounce_rate: number; sessions: number; }>([ + 'p.title', 'p.origin', 'p.path', 'p.avg_duration', diff --git a/packages/trpc/src/routers/event.ts b/packages/trpc/src/routers/event.ts index 5a169b51..c6b3b1ce 100644 --- a/packages/trpc/src/routers/event.ts +++ b/packages/trpc/src/routers/event.ts @@ -13,14 +13,20 @@ import { getEventList, getEvents, getTopPages, + overviewService, } from '@openpanel/db'; -import { zChartEventFilter } from '@openpanel/validation'; +import { + zChartEventFilter, + zRange, + zTimeInterval, +} from '@openpanel/validation'; -import { addMinutes, subMinutes } from 'date-fns'; +import { addMinutes, subDays, subMinutes } from 'date-fns'; import { clone } from 'ramda'; import { getProjectAccessCached } from '../access'; import { TRPCAccessError } from '../errors'; import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc'; +import { getChartStartEndDate } from './chart.helpers'; export const eventRouter = createTRPCRouter({ updateEventMeta: protectedProcedure @@ -228,10 +234,30 @@ export const eventRouter = createTRPCRouter({ cursor: z.number().optional(), take: z.number().default(20), search: z.string().optional(), + range: zRange, + interval: zTimeInterval, + filters: z.array(zChartEventFilter).default([]), }), ) .query(async ({ input }) => { - return getTopPages(input); + const { startDate, endDate } = getChartStartEndDate(input); + if (input.search) { + input.filters.push({ + id: 'path', + name: 'path', + value: [input.search], + operator: 'contains', + }); + } + return overviewService.getTopPages({ + projectId: input.projectId, + filters: input.filters, + startDate, + endDate, + interval: input.interval, + cursor: input.cursor || 1, + limit: input.take, + }); }), origin: protectedProcedure