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 (
+
+
+
+
+
+ {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