improve(dashboard): make pages page better (ux and features)

This commit is contained in:
Carl-Gerhard Lindesvärd
2025-04-17 09:27:50 +02:00
parent 89ab8d08de
commit d0e90dfa79
5 changed files with 179 additions and 150 deletions

View File

@@ -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 (
<div className="overflow-x-auto rounded-md border bg-background">
<div className={cn('min-w-[800px]')}>
<div className="grid grid-cols-[0.2fr_auto_1fr] overflow-hidden rounded-t-none border-b">
<div className="center-center h-10 rounded-tl-md bg-def-100 p-4 font-semibold text-muted-foreground">
Views
</div>
<div className="flex h-10 w-80 items-center bg-def-100 p-4 font-semibold text-muted-foreground">
Path
</div>
<div className="flex h-10 items-center rounded-tr-md bg-def-100 p-4 font-semibold text-muted-foreground">
Chart
</div>
</div>
{data.map((item, index) => {
return (
<div
key={item.path + item.origin + item.title}
className="grid grid-cols-[0.2fr_auto_1fr] border-b transition-colors last:border-b-0 hover:bg-muted/50 data-[state=selected]:bg-muted"
>
<div
className={cn(
cell,
'center-center font-mono text-lg font-semibold',
index === data.length - 1 && 'rounded-bl-md',
)}
>
{number.short(item.count)}
</div>
<div
className={cn(
cell,
'flex w-80 flex-col justify-center gap-2 text-left',
)}
>
<span className="truncate font-medium">{item.title}</span>
{item.origin ? (
<a
href={item.origin + item.path}
className="truncate font-mono text-sm text-muted-foreground underline"
>
<ExternalLinkIcon className="mr-2 inline-block size-3" />
{item.path}
</a>
) : (
<span className="truncate font-mono text-sm text-muted-foreground">
{item.path}
</span>
)}
</div>
<div
className={cn(
cell,
'p-1',
index === data.length - 1 && 'rounded-br-md',
)}
>
<ReportChart
options={{
hideID: true,
hideXAxis: true,
hideYAxis: true,
aspectRatio: 0.15,
}}
report={{
lineType: 'linear',
breakdowns: [],
name: 'screen_view',
metric: 'sum',
range: '30d',
interval: 'day',
previous: true,
chartType: 'linear',
projectId: item.project_id,
events: [
{
id: 'A',
name: 'screen_view',
segment: 'event',
filters: [
{
id: 'path',
name: 'path',
value: [item.path],
operator: 'is',
},
{
id: 'origin',
name: 'origin',
value: [item.origin],
operator: 'is',
},
],
},
],
}}
/>
</div>
</div>
);
})}
</div>
</div>
);
},
(prevProps, nextProps) => {
return isEqual(prevProps.data, nextProps.data);
},
);
PagesTable.displayName = 'PagesTable';

View File

@@ -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 (
<>
<TableButtons>
<OverviewRange />
<OverviewInterval />
<OverviewFiltersDrawer projectId={projectId} mode="events" />
<Input
className="self-auto"
placeholder="Search path"
value={search ?? ''}
onChange={(e) => {
@@ -47,19 +62,132 @@ export function Pages({ projectId }: { projectId: string }) {
}}
/>
</TableButtons>
{query.isLoading ? (
<TableSkeleton cols={3} />
) : (
<PagesTable data={data} />
)}
<Pagination
className="mt-2"
setCursor={setCursor}
cursor={cursor}
count={Number.POSITIVE_INFINITY}
take={take}
loading={query.isFetching}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{data.map((page) => {
return (
<PageCard
key={page.path}
page={page}
range={range}
interval={interval}
projectId={projectId}
/>
);
})}
</div>
<div className="p-4">
<Pagination
take={20}
count={9999}
cursor={cursor}
setCursor={setCursor}
className="self-auto"
size="base"
loading={query.isFetching}
/>
</div>
</>
);
}
const PageCard = memo(
({
page,
range,
interval,
projectId,
}: {
page: RouterOutputs['event']['pages'][number];
range: IChartRange;
interval: IInterval;
projectId: string;
}) => {
const number = useNumber();
return (
<div className="card">
<div className="row gap-4 justify-between p-4 py-2 items-center">
<div className="col min-w-0">
<div className="font-medium leading-[28px] truncate">
{page.title}
</div>
<a
target="_blank"
rel="noreferrer"
href={`${page.origin}${page.path}`}
className="text-muted-foreground font-mono truncate hover:underline"
>
{page.path}
</a>
</div>
</div>
<div className="row border-y">
<div className="center-center col flex-1 p-4 py-2">
<div className="font-medium text-xl font-mono">
{number.formatWithUnit(page.avg_duration, 'min')}
</div>
<div className="text-muted-foreground whitespace-nowrap text-sm">
duration
</div>
</div>
<div className="center-center col flex-1 p-4 py-2">
<div className="font-medium text-xl font-mono">
{number.formatWithUnit(page.bounce_rate / 100, '%')}
</div>
<div className="text-muted-foreground whitespace-nowrap text-sm">
bounce rate
</div>
</div>
<div className="center-center col flex-1 p-4 py-2">
<div className="font-medium text-xl font-mono">
{number.format(page.sessions)}
</div>
<div className="text-muted-foreground whitespace-nowrap text-sm">
sessions
</div>
</div>
</div>
<ReportChart
options={{
hideID: true,
hideXAxis: true,
hideYAxis: true,
aspectRatio: 0.15,
}}
report={{
lineType: 'linear',
breakdowns: [],
name: 'screen_view',
metric: 'sum',
range,
interval,
previous: true,
chartType: 'linear',
projectId,
events: [
{
id: 'A',
name: 'screen_view',
segment: 'event',
filters: [
{
id: 'path',
name: 'path',
value: [page.path],
operator: 'is',
},
{
id: 'origin',
name: 'origin',
value: [page.origin],
operator: 'is',
},
],
},
],
}}
/>
</div>
);
},
);