feat(dashboard): added new Pages
This commit is contained in:
@@ -17,7 +17,7 @@ const ListDashboardsServer = async ({ projectId }: Props) => {
|
||||
return (
|
||||
<Padding>
|
||||
<HeaderDashboards />
|
||||
<ListDashboards dashboards={dashboards} />;
|
||||
<ListDashboards dashboards={dashboards} />
|
||||
</Padding>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useUser } from '@clerk/nextjs';
|
||||
import {
|
||||
GanttChartIcon,
|
||||
Globe2Icon,
|
||||
LayersIcon,
|
||||
LayoutPanelTopIcon,
|
||||
PlusIcon,
|
||||
ScanEyeIcon,
|
||||
@@ -89,6 +90,11 @@ export default function LayoutMenu({ dashboards }: LayoutMenuProps) {
|
||||
label="Dashboards"
|
||||
href={`/${params.organizationSlug}/${projectId}/dashboards`}
|
||||
/>
|
||||
<LinkWithIcon
|
||||
icon={LayersIcon}
|
||||
label="Pages"
|
||||
href={`/${params.organizationSlug}/${projectId}/pages`}
|
||||
/>
|
||||
<LinkWithIcon
|
||||
icon={Globe2Icon}
|
||||
label="Realtime"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { PageTabs, PageTabsLink } from '@/components/page-tabs';
|
||||
import { Padding } from '@/components/ui/padding';
|
||||
import { parseAsStringEnum } from 'nuqs';
|
||||
|
||||
import { Pages } from './pages';
|
||||
|
||||
interface PageProps {
|
||||
params: {
|
||||
projectId: string;
|
||||
};
|
||||
searchParams: {
|
||||
tab: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function Page({
|
||||
params: { projectId },
|
||||
searchParams,
|
||||
}: PageProps) {
|
||||
const tab = parseAsStringEnum(['pages', 'trends'])
|
||||
.withDefault('pages')
|
||||
.parseServerSide(searchParams.tab);
|
||||
|
||||
return (
|
||||
<Padding>
|
||||
<PageTabs className="mb-4">
|
||||
<PageTabsLink href="?tab=pages" isActive={tab === 'pages'}>
|
||||
Pages
|
||||
</PageTabsLink>
|
||||
</PageTabs>
|
||||
{tab === 'pages' && <Pages projectId={projectId} />}
|
||||
</Padding>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import { LazyChart } from '@/components/report/chart/LazyChart';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ExternalLinkIcon } from 'lucide-react';
|
||||
|
||||
import type { IServicePage } from '@openpanel/db';
|
||||
|
||||
export function PagesTable({ 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'
|
||||
)}
|
||||
>
|
||||
<LazyChart
|
||||
hideYAxis
|
||||
hideXAxis
|
||||
className="w-full"
|
||||
lineType="linear"
|
||||
breakdowns={[]}
|
||||
name="screen_view"
|
||||
metric="sum"
|
||||
range="30d"
|
||||
interval="day"
|
||||
previous
|
||||
aspectRatio={0.15}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
'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 { parseAsInteger, useQueryState } from 'nuqs';
|
||||
|
||||
import { PagesTable } from './pages-table';
|
||||
|
||||
export function Pages({ projectId }: { projectId: string }) {
|
||||
const take = 20;
|
||||
const [cursor, setCursor] = useQueryState(
|
||||
'cursor',
|
||||
parseAsInteger.withDefault(0)
|
||||
);
|
||||
const [search, setSearch] = useQueryState('search', {
|
||||
defaultValue: '',
|
||||
shallow: true,
|
||||
});
|
||||
const debouncedSearch = useDebounceValue(search, 500);
|
||||
const query = api.event.pages.useQuery(
|
||||
{
|
||||
projectId,
|
||||
cursor,
|
||||
take,
|
||||
search: debouncedSearch,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
const data = query.data ?? [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableButtons>
|
||||
<Input
|
||||
placeholder="Serch path"
|
||||
value={search ?? ''}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setCursor(0);
|
||||
}}
|
||||
/>
|
||||
</TableButtons>
|
||||
{query.isLoading ? (
|
||||
<TableSkeleton cols={3} />
|
||||
) : (
|
||||
<PagesTable data={data} />
|
||||
)}
|
||||
<Pagination
|
||||
className="mt-2"
|
||||
setCursor={setCursor}
|
||||
cursor={cursor}
|
||||
count={Infinity}
|
||||
take={take}
|
||||
loading={query.isFetching}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user