feat(dashboard): added new Pages
This commit is contained in:
@@ -34,6 +34,7 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
|
"@radix-ui/react-portal": "^1.1.1",
|
||||||
"@radix-ui/react-progress": "^1.0.3",
|
"@radix-ui/react-progress": "^1.0.3",
|
||||||
"@radix-ui/react-radio-group": "^1.1.3",
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const ListDashboardsServer = async ({ projectId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Padding>
|
<Padding>
|
||||||
<HeaderDashboards />
|
<HeaderDashboards />
|
||||||
<ListDashboards dashboards={dashboards} />;
|
<ListDashboards dashboards={dashboards} />
|
||||||
</Padding>
|
</Padding>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useUser } from '@clerk/nextjs';
|
|||||||
import {
|
import {
|
||||||
GanttChartIcon,
|
GanttChartIcon,
|
||||||
Globe2Icon,
|
Globe2Icon,
|
||||||
|
LayersIcon,
|
||||||
LayoutPanelTopIcon,
|
LayoutPanelTopIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
ScanEyeIcon,
|
ScanEyeIcon,
|
||||||
@@ -89,6 +90,11 @@ export default function LayoutMenu({ dashboards }: LayoutMenuProps) {
|
|||||||
label="Dashboards"
|
label="Dashboards"
|
||||||
href={`/${params.organizationSlug}/${projectId}/dashboards`}
|
href={`/${params.organizationSlug}/${projectId}/dashboards`}
|
||||||
/>
|
/>
|
||||||
|
<LinkWithIcon
|
||||||
|
icon={LayersIcon}
|
||||||
|
label="Pages"
|
||||||
|
href={`/${params.organizationSlug}/${projectId}/pages`}
|
||||||
|
/>
|
||||||
<LinkWithIcon
|
<LinkWithIcon
|
||||||
icon={Globe2Icon}
|
icon={Globe2Icon}
|
||||||
label="Realtime"
|
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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,8 +3,10 @@ import { DataTable } from '@/components/data-table';
|
|||||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||||
import { Pagination } from '@/components/pagination';
|
import { Pagination } from '@/components/pagination';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { TableSkeleton } from '@/components/ui/table';
|
||||||
import type { UseQueryResult } from '@tanstack/react-query';
|
import type { UseQueryResult } from '@tanstack/react-query';
|
||||||
import { GanttChartIcon } from 'lucide-react';
|
import { GanttChartIcon } from 'lucide-react';
|
||||||
|
import { column } from 'mathjs';
|
||||||
|
|
||||||
import type { IServiceEvent } from '@openpanel/db';
|
import type { IServiceEvent } from '@openpanel/db';
|
||||||
|
|
||||||
@@ -25,15 +27,7 @@ export const EventsTable = ({ query, ...props }: Props) => {
|
|||||||
const { data, isFetching, isLoading } = query;
|
const { data, isFetching, isLoading } = query;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return <TableSkeleton cols={columns.length} />;
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
|
||||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
|
||||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
|
||||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
|
||||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data?.length === 0) {
|
if (data?.length === 0) {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const GridCell: React.FC<
|
|||||||
}) => (
|
}) => (
|
||||||
<Component
|
<Component
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-12 items-center whitespace-nowrap px-4 align-middle shadow-[0_0_0_0.5px] shadow-border',
|
'flex min-h-12 items-center whitespace-nowrap px-4 align-middle shadow-[0_0_0_0.5px] shadow-border',
|
||||||
isHeader && 'h-10 bg-def-100 font-semibold text-muted-foreground',
|
isHeader && 'h-10 bg-def-100 font-semibold text-muted-foreground',
|
||||||
colSpan && `col-span-${colSpan}`,
|
colSpan && `col-span-${colSpan}`,
|
||||||
className
|
className
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function PageTabs({
|
|||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={cn('overflow-x-auto', className)}>
|
<div className={cn('h-7 overflow-x-auto', className)}>
|
||||||
<div className="flex gap-4 whitespace-nowrap text-3xl font-semibold">
|
<div className="flex gap-4 whitespace-nowrap text-3xl font-semibold">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { DataTable } from '@/components/data-table';
|
|||||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||||
import { Pagination } from '@/components/pagination';
|
import { Pagination } from '@/components/pagination';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { TableSkeleton } from '@/components/ui/table';
|
||||||
import type { UseQueryResult } from '@tanstack/react-query';
|
import type { UseQueryResult } from '@tanstack/react-query';
|
||||||
import { GanttChartIcon } from 'lucide-react';
|
import { GanttChartIcon } from 'lucide-react';
|
||||||
|
|
||||||
@@ -26,15 +27,7 @@ export const ProfilesTable = ({ type, query, ...props }: Props) => {
|
|||||||
const { data, isFetching, isLoading } = query;
|
const { data, isFetching, isLoading } = query;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return <TableSkeleton cols={columns.length} />;
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
|
||||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
|
||||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
|
||||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
|
||||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data?.length === 0) {
|
if (data?.length === 0) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||||
import { cn } from '@/utils/cn';
|
|
||||||
|
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
import { MetricCardEmpty } from './MetricCard';
|
import { MetricCardEmpty } from './MetricCard';
|
||||||
|
import { ResponsiveContainer } from './ResponsiveContainer';
|
||||||
|
|
||||||
export function ChartEmpty() {
|
export function ChartEmpty() {
|
||||||
const { editMode, chartType } = useChartContext();
|
const { editMode, chartType } = useChartContext();
|
||||||
@@ -20,12 +20,10 @@ export function ChartEmpty() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<ResponsiveContainer>
|
||||||
className={
|
<div className={'flex h-full w-full items-center justify-center'}>
|
||||||
'flex aspect-video max-h-[300px] min-h-[200px] w-full items-center justify-center'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
No data
|
No data
|
||||||
</div>
|
</div>
|
||||||
|
</ResponsiveContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
|
import { ResponsiveContainer } from './ResponsiveContainer';
|
||||||
|
|
||||||
interface ChartLoadingProps {
|
interface ChartLoadingProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
aspectRatio?: number;
|
||||||
}
|
}
|
||||||
export function ChartLoading({ className }: ChartLoadingProps) {
|
export function ChartLoading({ className, aspectRatio }: ChartLoadingProps) {
|
||||||
return (
|
return (
|
||||||
|
<ResponsiveContainer aspectRatio={aspectRatio}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-def-200 aspect-video max-h-[300px] min-h-[200px] w-full animate-pulse rounded',
|
'h-full w-full animate-pulse rounded bg-def-200',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</ResponsiveContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import type { LucideIcon } from 'lucide-react';
|
|||||||
import type { IChartProps, IChartSerie } from '@openpanel/validation';
|
import type { IChartProps, IChartSerie } from '@openpanel/validation';
|
||||||
|
|
||||||
export interface IChartContextType extends IChartProps {
|
export interface IChartContextType extends IChartProps {
|
||||||
|
hideXAxis?: boolean;
|
||||||
|
hideYAxis?: boolean;
|
||||||
|
aspectRatio?: number;
|
||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
hideID?: boolean;
|
hideID?: boolean;
|
||||||
onClick?: (item: IChartSerie) => void;
|
onClick?: (item: IChartSerie) => void;
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { cn } from '@/utils/cn';
|
||||||
import { useInViewport } from 'react-in-viewport';
|
import { useInViewport } from 'react-in-viewport';
|
||||||
|
|
||||||
import type { IChartRoot } from '.';
|
import type { IChartRoot } from '.';
|
||||||
import { ChartRoot } from '.';
|
import { ChartRoot } from '.';
|
||||||
import { ChartLoading } from './ChartLoading';
|
import { ChartLoading } from './ChartLoading';
|
||||||
|
|
||||||
export function LazyChart(props: IChartRoot) {
|
export function LazyChart({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: IChartRoot & { className?: string }) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const once = useRef(false);
|
const once = useRef(false);
|
||||||
const { inViewport } = useInViewport(ref, undefined, {
|
const { inViewport } = useInViewport(ref, undefined, {
|
||||||
@@ -21,11 +25,11 @@ export function LazyChart(props: IChartRoot) {
|
|||||||
}, [inViewport]);
|
}, [inViewport]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref}>
|
<div ref={ref} className={cn('w-full', className)}>
|
||||||
{once.current || inViewport ? (
|
{once.current || inViewport ? (
|
||||||
<ChartRoot {...props} editMode={false} />
|
<ChartRoot {...props} editMode={false} />
|
||||||
) : (
|
) : (
|
||||||
<ChartLoading />
|
<ChartLoading aspectRatio={props.aspectRatio} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||||
import { useMappings } from '@/hooks/useMappings';
|
|
||||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||||
import type { IRechartPayloadItem } from '@/hooks/useRechartDataModel';
|
import type { IRechartPayloadItem } from '@/hooks/useRechartDataModel';
|
||||||
import type { IToolTipProps } from '@/types';
|
import type { IToolTipProps } from '@/types';
|
||||||
|
import * as Portal from '@radix-ui/react-portal';
|
||||||
|
import { bind } from 'bind-event-listener';
|
||||||
|
import throttle from 'lodash.throttle';
|
||||||
|
|
||||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
@@ -24,11 +26,35 @@ export function ReportChartTooltip({
|
|||||||
const { unit, interval } = useChartContext();
|
const { unit, interval } = useChartContext();
|
||||||
const formatDate = useFormatDateInterval(interval);
|
const formatDate = useFormatDateInterval(interval);
|
||||||
const number = useNumber();
|
const number = useNumber();
|
||||||
if (!active || !payload) {
|
const [position, setPosition] = useState<{ x: number; y: number } | null>(
|
||||||
return null;
|
null
|
||||||
}
|
);
|
||||||
|
|
||||||
if (!payload.length) {
|
const inactive = !active || !payload?.length;
|
||||||
|
useEffect(() => {
|
||||||
|
const setPositionThrottled = throttle(setPosition, 50);
|
||||||
|
const unsubMouseMove = bind(window, {
|
||||||
|
type: 'mousemove',
|
||||||
|
listener(event) {
|
||||||
|
if (!inactive) {
|
||||||
|
setPositionThrottled({ x: event.clientX, y: event.clientY + 20 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const unsubDragEnter = bind(window, {
|
||||||
|
type: 'pointerdown',
|
||||||
|
listener() {
|
||||||
|
setPosition(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubMouseMove();
|
||||||
|
unsubDragEnter();
|
||||||
|
};
|
||||||
|
}, [inactive]);
|
||||||
|
|
||||||
|
if (inactive) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +67,30 @@ export function ReportChartTooltip({
|
|||||||
const visible = sorted.slice(0, limit);
|
const visible = sorted.slice(0, limit);
|
||||||
const hidden = sorted.slice(limit);
|
const hidden = sorted.slice(limit);
|
||||||
|
|
||||||
|
const correctXPosition = (x: number | undefined) => {
|
||||||
|
if (!x) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipWidth = 300;
|
||||||
|
const screenWidth = window.innerWidth;
|
||||||
|
const newX = x;
|
||||||
|
|
||||||
|
if (newX + tooltipWidth > screenWidth) {
|
||||||
|
return screenWidth - tooltipWidth;
|
||||||
|
}
|
||||||
|
return newX;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Portal.Portal
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: position?.y,
|
||||||
|
left: correctXPosition(position?.x),
|
||||||
|
zIndex: 1000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex min-w-[180px] flex-col gap-2 rounded-xl border bg-card p-3 shadow-xl">
|
<div className="flex min-w-[180px] flex-col gap-2 rounded-xl border bg-card p-3 shadow-xl">
|
||||||
{visible.map((item, index) => {
|
{visible.map((item, index) => {
|
||||||
// If we have a <Cell /> component, payload can be nested
|
// If we have a <Cell /> component, payload can be nested
|
||||||
@@ -70,7 +119,7 @@ export function ReportChartTooltip({
|
|||||||
<SerieIcon name={data.names} />
|
<SerieIcon name={data.names} />
|
||||||
<SerieName name={data.names} />
|
<SerieName name={data.names} />
|
||||||
</div>
|
</div>
|
||||||
<div className="font-mono flex justify-between gap-8 font-medium">
|
<div className="flex justify-between gap-8 font-mono font-medium">
|
||||||
<div className="row gap-1">
|
<div className="row gap-1">
|
||||||
{number.formatWithUnit(data.count, unit)}
|
{number.formatWithUnit(data.count, unit)}
|
||||||
{!!data.previous && (
|
{!!data.previous && (
|
||||||
@@ -88,8 +137,11 @@ export function ReportChartTooltip({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{hidden.length > 0 && (
|
{hidden.length > 0 && (
|
||||||
<div className="text-muted-foreground">and {hidden.length} more...</div>
|
<div className="text-muted-foreground">
|
||||||
|
and {hidden.length} more...
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</Portal.Portal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
|||||||
import type { IChartData } from '@/trpc/client';
|
import type { IChartData } from '@/trpc/client';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { getChartColor, theme } from '@/utils/theme';
|
import { getChartColor, theme } from '@/utils/theme';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
|
import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
|
||||||
|
|
||||||
import type { IInterval } from '@openpanel/validation';
|
|
||||||
|
|
||||||
import { getYAxisWidth } from './chart-utils';
|
import { getYAxisWidth } from './chart-utils';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||||
@@ -21,7 +20,11 @@ interface ReportHistogramChartProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function BarHover({ x, y, width, height, top, left, right, bottom }: any) {
|
function BarHover({ x, y, width, height, top, left, right, bottom }: any) {
|
||||||
const bg = theme?.colors?.slate?.['200'] as string;
|
const themeMode = useTheme();
|
||||||
|
const bg =
|
||||||
|
themeMode?.theme === 'dark'
|
||||||
|
? theme.colors['def-100']
|
||||||
|
: theme.colors['def-300'];
|
||||||
return (
|
return (
|
||||||
<rect
|
<rect
|
||||||
{...{ x, y, width, height, top, left, right, bottom }}
|
{...{ x, y, width, height, top, left, right, bottom }}
|
||||||
@@ -33,7 +36,7 @@ function BarHover({ x, y, width, height, top, left, right, bottom }: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ReportHistogramChart({ data }: ReportHistogramChartProps) {
|
export function ReportHistogramChart({ data }: ReportHistogramChartProps) {
|
||||||
const { editMode, previous, interval } = useChartContext();
|
const { editMode, previous, interval, aspectRatio } = useChartContext();
|
||||||
const formatDate = useFormatDateInterval(interval);
|
const formatDate = useFormatDateInterval(interval);
|
||||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||||
const rechartData = useRechartDataModel(series);
|
const rechartData = useRechartDataModel(series);
|
||||||
@@ -41,10 +44,15 @@ export function ReportHistogramChart({ data }: ReportHistogramChartProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={cn(editMode && 'card p-4')}>
|
<div className={cn('w-full', editMode && 'card p-4')}>
|
||||||
<ResponsiveContainer>
|
<ResponsiveContainer aspectRatio={aspectRatio}>
|
||||||
{({ width, height }) => (
|
{({ width, height }) => (
|
||||||
<BarChart width={width} height={height} data={rechartData}>
|
<BarChart
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
data={rechartData}
|
||||||
|
barCategoryGap={10}
|
||||||
|
>
|
||||||
<CartesianGrid
|
<CartesianGrid
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
vertical={false}
|
vertical={false}
|
||||||
@@ -70,22 +78,45 @@ export function ReportHistogramChart({ data }: ReportHistogramChartProps) {
|
|||||||
{series.map((serie) => {
|
{series.map((serie) => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={serie.id}>
|
<React.Fragment key={serie.id}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="colorGradient"
|
||||||
|
x1="0"
|
||||||
|
y1="1"
|
||||||
|
x2="0"
|
||||||
|
y2="0"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset="0%"
|
||||||
|
stopColor={getChartColor(serie.index)}
|
||||||
|
stopOpacity={0.7}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset="100%"
|
||||||
|
stopColor={getChartColor(serie.index)}
|
||||||
|
stopOpacity={1}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
{previous && (
|
{previous && (
|
||||||
<Bar
|
<Bar
|
||||||
key={`${serie.id}:prev`}
|
key={`${serie.id}:prev`}
|
||||||
name={`${serie.id}:prev`}
|
name={`${serie.id}:prev`}
|
||||||
dataKey={`${serie.id}:prev:count`}
|
dataKey={`${serie.id}:prev:count`}
|
||||||
fill={getChartColor(serie.index)}
|
fill={getChartColor(serie.index)}
|
||||||
fillOpacity={0.2}
|
fillOpacity={0.1}
|
||||||
radius={3}
|
radius={3}
|
||||||
|
barSize={20} // Adjust the bar width here
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Bar
|
<Bar
|
||||||
key={serie.id}
|
key={serie.id}
|
||||||
name={serie.id}
|
name={serie.id}
|
||||||
dataKey={`${serie.id}:count`}
|
dataKey={`${serie.id}:count`}
|
||||||
fill={getChartColor(serie.index)}
|
fill="url(#colorGradient)"
|
||||||
radius={3}
|
radius={3}
|
||||||
|
fillOpacity={1}
|
||||||
|
barSize={20} // Adjust the bar width here
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -45,7 +45,11 @@ export function ReportLineChart({ data }: ReportLineChartProps) {
|
|||||||
endDate,
|
endDate,
|
||||||
range,
|
range,
|
||||||
lineType,
|
lineType,
|
||||||
|
aspectRatio,
|
||||||
|
hideXAxis,
|
||||||
|
hideYAxis,
|
||||||
} = useChartContext();
|
} = useChartContext();
|
||||||
|
const dataLength = data.series[0]?.data?.length || 0;
|
||||||
const references = api.reference.getChartReferences.useQuery(
|
const references = api.reference.getChartReferences.useQuery(
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
@@ -120,11 +124,10 @@ export function ReportLineChart({ data }: ReportLineChartProps) {
|
|||||||
}, [series]);
|
}, [series]);
|
||||||
|
|
||||||
const isAreaStyle = series.length === 1;
|
const isAreaStyle = series.length === 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={cn(editMode && 'card p-4')}>
|
<div className={cn('w-full', editMode && 'card p-4')}>
|
||||||
<ResponsiveContainer>
|
<ResponsiveContainer aspectRatio={aspectRatio}>
|
||||||
{({ width, height }) => (
|
{({ width, height }) => (
|
||||||
<ComposedChart width={width} height={height} data={rechartData}>
|
<ComposedChart width={width} height={height} data={rechartData}>
|
||||||
<CartesianGrid
|
<CartesianGrid
|
||||||
@@ -149,7 +152,7 @@ export function ReportLineChart({ data }: ReportLineChartProps) {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<YAxis
|
<YAxis
|
||||||
width={getYAxisWidth(data.metrics.max)}
|
width={hideYAxis ? 0 : getYAxisWidth(data.metrics.max)}
|
||||||
fontSize={12}
|
fontSize={12}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
@@ -164,6 +167,7 @@ export function ReportLineChart({ data }: ReportLineChartProps) {
|
|||||||
)}
|
)}
|
||||||
<Tooltip content={<ReportChartTooltip />} />
|
<Tooltip content={<ReportChartTooltip />} />
|
||||||
<XAxis
|
<XAxis
|
||||||
|
height={hideXAxis ? 0 : undefined}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
fontSize={12}
|
fontSize={12}
|
||||||
dataKey="timestamp"
|
dataKey="timestamp"
|
||||||
@@ -212,7 +216,7 @@ export function ReportLineChart({ data }: ReportLineChartProps) {
|
|||||||
)}
|
)}
|
||||||
</defs>
|
</defs>
|
||||||
<Line
|
<Line
|
||||||
dot={isAreaStyle}
|
dot={isAreaStyle && dataLength <= 8}
|
||||||
type={lineType}
|
type={lineType}
|
||||||
name={serie.id}
|
name={serie.id}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
|
|||||||
@@ -1,28 +1,45 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
|
||||||
|
import { DEFAULT_ASPECT_RATIO } from '@openpanel/constants';
|
||||||
|
|
||||||
interface ResponsiveContainerProps {
|
interface ResponsiveContainerProps {
|
||||||
children: (props: { width: number; height: number }) => React.ReactNode;
|
aspectRatio?: number;
|
||||||
|
children:
|
||||||
|
| ((props: { width: number; height: number }) => React.ReactNode)
|
||||||
|
| React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ResponsiveContainer({ children }: ResponsiveContainerProps) {
|
export function ResponsiveContainer({
|
||||||
|
children,
|
||||||
|
aspectRatio = 0.5625,
|
||||||
|
}: ResponsiveContainerProps) {
|
||||||
const maxHeight = 300;
|
const maxHeight = 300;
|
||||||
const minHeight = 200;
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
className="w-full"
|
||||||
style={{
|
style={{
|
||||||
|
aspectRatio: 1 / (aspectRatio || DEFAULT_ASPECT_RATIO),
|
||||||
maxHeight,
|
maxHeight,
|
||||||
minHeight,
|
|
||||||
}}
|
}}
|
||||||
className={'aspect-video w-full max-sm:-mx-3'}
|
|
||||||
>
|
>
|
||||||
|
{typeof children === 'function' ? (
|
||||||
<AutoSizer disableHeight>
|
<AutoSizer disableHeight>
|
||||||
{({ width }) =>
|
{({ width }) =>
|
||||||
children({
|
children({
|
||||||
width,
|
width,
|
||||||
height: Math.min(maxHeight, width * 0.5625),
|
height: Math.min(
|
||||||
|
maxHeight,
|
||||||
|
width * aspectRatio || DEFAULT_ASPECT_RATIO
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Suspense, useEffect, useState } from 'react';
|
import { Suspense, useEffect, useState } from 'react';
|
||||||
|
import * as Portal from '@radix-ui/react-portal';
|
||||||
|
|
||||||
import type { IChartProps } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
@@ -24,14 +25,18 @@ export function ChartRoot(props: IChartContextType) {
|
|||||||
return props.chartType === 'metric' ? (
|
return props.chartType === 'metric' ? (
|
||||||
<MetricCardLoading />
|
<MetricCardLoading />
|
||||||
) : (
|
) : (
|
||||||
<ChartLoading />
|
<ChartLoading aspectRatio={props.aspectRatio} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
props.chartType === 'metric' ? <MetricCardLoading /> : <ChartLoading />
|
props.chartType === 'metric' ? (
|
||||||
|
<MetricCardLoading />
|
||||||
|
) : (
|
||||||
|
<ChartLoading aspectRatio={props.aspectRatio} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ChartProvider {...props}>
|
<ChartProvider {...props}>
|
||||||
@@ -49,9 +54,13 @@ interface ChartRootShortcutProps {
|
|||||||
interval?: IChartProps['interval'];
|
interval?: IChartProps['interval'];
|
||||||
events: IChartProps['events'];
|
events: IChartProps['events'];
|
||||||
breakdowns?: IChartProps['breakdowns'];
|
breakdowns?: IChartProps['breakdowns'];
|
||||||
|
lineType?: IChartProps['lineType'];
|
||||||
|
hideXAxis?: boolean;
|
||||||
|
aspectRatio?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChartRootShortcut = ({
|
export const ChartRootShortcut = ({
|
||||||
|
hideXAxis,
|
||||||
projectId,
|
projectId,
|
||||||
range = '7d',
|
range = '7d',
|
||||||
previous = false,
|
previous = false,
|
||||||
@@ -59,8 +68,11 @@ export const ChartRootShortcut = ({
|
|||||||
interval = 'day',
|
interval = 'day',
|
||||||
events,
|
events,
|
||||||
breakdowns,
|
breakdowns,
|
||||||
|
aspectRatio,
|
||||||
|
lineType = 'monotone',
|
||||||
}: ChartRootShortcutProps) => {
|
}: ChartRootShortcutProps) => {
|
||||||
return (
|
return (
|
||||||
|
<Portal.Root>
|
||||||
<ChartRoot
|
<ChartRoot
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
range={range}
|
range={range}
|
||||||
@@ -69,9 +81,12 @@ export const ChartRootShortcut = ({
|
|||||||
chartType={chartType}
|
chartType={chartType}
|
||||||
interval={interval}
|
interval={interval}
|
||||||
name="Random"
|
name="Random"
|
||||||
lineType="bump"
|
lineType={lineType}
|
||||||
metric="sum"
|
metric="sum"
|
||||||
events={events}
|
events={events}
|
||||||
|
aspectRatio={aspectRatio}
|
||||||
|
hideXAxis={hideXAxis}
|
||||||
/>
|
/>
|
||||||
|
</Portal.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -110,6 +110,39 @@ const TableCaption = React.forwardRef<
|
|||||||
));
|
));
|
||||||
TableCaption.displayName = 'TableCaption';
|
TableCaption.displayName = 'TableCaption';
|
||||||
|
|
||||||
|
export function TableSkeleton({
|
||||||
|
rows = 10,
|
||||||
|
cols = 2,
|
||||||
|
}: {
|
||||||
|
rows?: number;
|
||||||
|
cols?: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
{Array.from({ length: cols }).map((_, j) => (
|
||||||
|
<TableHead key={j}>
|
||||||
|
<div className="h-4 w-1/4 animate-pulse rounded-full bg-def-300" />
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{Array.from({ length: rows }).map((_, i) => (
|
||||||
|
<TableRow key={i}>
|
||||||
|
{Array.from({ length: cols }).map((_, j) => (
|
||||||
|
<TableCell key={j}>
|
||||||
|
<div className="h-4 w-1/4 min-w-20 animate-pulse rounded-full bg-def-300" />
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function WidgetTitle({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{Icon && (
|
{Icon && (
|
||||||
<div className="bg-def-200 absolute left-0 rounded-lg p-2">
|
<div className="absolute left-0 rounded-lg bg-def-200 p-2">
|
||||||
<Icon size={18} />
|
<Icon size={18} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { isSameDay, isSameMonth } from 'date-fns';
|
import { isSameDay, isSameMonth } from 'date-fns';
|
||||||
|
|
||||||
|
export const DEFAULT_ASPECT_RATIO = 0.5625;
|
||||||
export const NOT_SET_VALUE = '(not set)';
|
export const NOT_SET_VALUE = '(not set)';
|
||||||
|
|
||||||
export const timeWindows = {
|
export const timeWindows = {
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ export type IImportedEvent = Omit<
|
|||||||
properties: Record<string, unknown>;
|
properties: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type IServicePage = {
|
||||||
|
path: string;
|
||||||
|
count: number;
|
||||||
|
project_id: string;
|
||||||
|
first_seen: string;
|
||||||
|
title: string;
|
||||||
|
origin: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IClickhouseEvent {
|
export interface IClickhouseEvent {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -493,3 +502,30 @@ export async function getLastScreenViewFromProfileId({
|
|||||||
|
|
||||||
return eventInDb || null;
|
return eventInDb || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getTopPages({
|
||||||
|
projectId,
|
||||||
|
cursor,
|
||||||
|
take,
|
||||||
|
search,
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
cursor?: number;
|
||||||
|
take: number;
|
||||||
|
search?: string;
|
||||||
|
}) {
|
||||||
|
const res = await chQuery<IServicePage>(`
|
||||||
|
SELECT path, count(*) as count, project_id, first_value(created_at) as first_seen, max(properties['__title']) as title, origin
|
||||||
|
FROM events_v2
|
||||||
|
WHERE name = 'screen_view'
|
||||||
|
AND project_id = ${escape(projectId)}
|
||||||
|
AND created_at > now() - INTERVAL 30 DAY
|
||||||
|
${search ? `AND path LIKE '%${search}%'` : ''}
|
||||||
|
GROUP BY path, project_id, origin
|
||||||
|
ORDER BY count desc
|
||||||
|
LIMIT ${take}
|
||||||
|
OFFSET ${Math.max(0, (cursor ?? 0) * take)}
|
||||||
|
`);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
db,
|
db,
|
||||||
getEventList,
|
getEventList,
|
||||||
getEvents,
|
getEvents,
|
||||||
|
getTopPages,
|
||||||
TABLE_NAMES,
|
TABLE_NAMES,
|
||||||
} from '@openpanel/db';
|
} from '@openpanel/db';
|
||||||
import { zChartEventFilter } from '@openpanel/validation';
|
import { zChartEventFilter } from '@openpanel/validation';
|
||||||
@@ -69,7 +70,6 @@ export const eventRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
cursor: z.number().optional(),
|
cursor: z.number().optional(),
|
||||||
limit: z.number().default(8),
|
|
||||||
profileId: z.string().optional(),
|
profileId: z.string().optional(),
|
||||||
take: z.number().default(50),
|
take: z.number().default(50),
|
||||||
events: z.array(z.string()).optional(),
|
events: z.array(z.string()).optional(),
|
||||||
@@ -165,4 +165,17 @@ export const eventRouter = createTRPCRouter({
|
|||||||
count: counts[0]?.count ?? 0,
|
count: counts[0]?.count ?? 0,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
pages: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
projectId: z.string(),
|
||||||
|
cursor: z.number().optional(),
|
||||||
|
take: z.number().default(20),
|
||||||
|
search: z.string().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return getTopPages(input);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { z } from 'zod';
|
|||||||
import { db } from '@openpanel/db';
|
import { db } from '@openpanel/db';
|
||||||
import { zInviteUser } from '@openpanel/validation';
|
import { zInviteUser } from '@openpanel/validation';
|
||||||
|
|
||||||
import { getOrganizationAccess, getOrganizationAccessCached } from '../access';
|
import { getOrganizationAccess } from '../access';
|
||||||
import { TRPCAccessError } from '../errors';
|
import { TRPCAccessError } from '../errors';
|
||||||
import { createTRPCRouter, protectedProcedure } from '../trpc';
|
import { createTRPCRouter, protectedProcedure } from '../trpc';
|
||||||
|
|
||||||
|
|||||||
84
pnpm-lock.yaml
generated
84
pnpm-lock.yaml
generated
@@ -233,6 +233,9 @@ importers:
|
|||||||
'@radix-ui/react-popover':
|
'@radix-ui/react-popover':
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.7(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-portal':
|
||||||
|
specifier: ^1.1.1
|
||||||
|
version: 1.1.1(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-progress':
|
'@radix-ui/react-progress':
|
||||||
specifier: ^1.0.3
|
specifier: ^1.0.3
|
||||||
version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -5574,6 +5577,19 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-compose-refs@1.1.0(@types/react@18.2.56)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 18.2.56
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-context@1.0.0(react@18.2.0):
|
/@radix-ui/react-context@1.0.0(react@18.2.0):
|
||||||
resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==}
|
resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5982,6 +5998,27 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-portal@1.1.1(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@types/react': 18.2.56
|
||||||
|
'@types/react-dom': 18.2.19
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-presence@1.0.0(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-presence@1.0.0(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==}
|
resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6050,6 +6087,26 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-primitive@2.0.0(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-slot': 1.1.0(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@types/react': 18.2.56
|
||||||
|
'@types/react-dom': 18.2.19
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-progress@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-progress@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==}
|
resolution: {integrity: sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6185,6 +6242,20 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-slot@1.1.0(@types/react@18.2.56)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@types/react': 18.2.56
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-switch@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-switch@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==}
|
resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6450,6 +6521,19 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.2.56)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 18.2.56
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-use-previous@1.0.1(@types/react@18.2.56)(react@18.2.0):
|
/@radix-ui/react-use-previous@1.0.1(@types/react@18.2.56)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==}
|
resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user