* esm * wip * wip * wip * wip * wip * wip * subscription notice * wip * wip * wip * fix envs * fix: update docker build * fix * esm/types * delete dashboard :D * add patches to dockerfiles * update packages + catalogs + ts * wip * remove native libs * ts * improvements * fix redirects and fetching session * try fix favicon * fixes * fix * order and resize reportds within a dashboard * improvements * wip * added userjot to dashboard * fix * add op * wip * different cache key * improve date picker * fix table * event details loading * redo onboarding completely * fix login * fix * fix * extend session, billing and improve bars * fix * reduce price on 10M
276 lines
7.8 KiB
TypeScript
276 lines
7.8 KiB
TypeScript
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
|
import { useEventQueryFilters } from '@/hooks/use-event-query-filters';
|
|
import { cn } from '@/utils/cn';
|
|
|
|
import { useDashedStroke } from '@/hooks/use-dashed-stroke';
|
|
import { useFormatDateInterval } from '@/hooks/use-format-date-interval';
|
|
import { useNumber } from '@/hooks/use-numer-formatter';
|
|
import { useTRPC } from '@/integrations/trpc/react';
|
|
import type { RouterOutputs } from '@/trpc/client';
|
|
import { getChartColor } from '@/utils/theme';
|
|
import { getPreviousMetric } from '@openpanel/common';
|
|
import type { IInterval } from '@openpanel/validation';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { isSameDay, isSameHour, isSameMonth, isSameWeek } from 'date-fns';
|
|
import { last, omit } from 'ramda';
|
|
import React, { useState } from 'react';
|
|
import {
|
|
Bar,
|
|
CartesianGrid,
|
|
ComposedChart,
|
|
Line,
|
|
ResponsiveContainer,
|
|
XAxis,
|
|
YAxis,
|
|
} from 'recharts';
|
|
import { createChartTooltip } from '../charts/chart-tooltip';
|
|
import { BarShapeBlue } from '../charts/common-bar';
|
|
import { useXAxisProps, useYAxisProps } from '../report-chart/common/axis';
|
|
import { PreviousDiffIndicatorPure } from '../report-chart/common/previous-diff-indicator';
|
|
import { Skeleton } from '../skeleton';
|
|
import { OverviewLiveHistogram } from './overview-live-histogram';
|
|
import { OverviewMetricCard } from './overview-metric-card';
|
|
|
|
interface OverviewMetricsProps {
|
|
projectId: string;
|
|
}
|
|
|
|
const TITLES = [
|
|
{
|
|
title: 'Unique Visitors',
|
|
key: 'unique_visitors',
|
|
unit: '',
|
|
inverted: false,
|
|
},
|
|
{
|
|
title: 'Sessions',
|
|
key: 'total_sessions',
|
|
unit: '',
|
|
inverted: false,
|
|
},
|
|
{
|
|
title: 'Pageviews',
|
|
key: 'total_screen_views',
|
|
unit: '',
|
|
inverted: false,
|
|
},
|
|
{
|
|
title: 'Pages per session',
|
|
key: 'views_per_session',
|
|
unit: '',
|
|
inverted: false,
|
|
},
|
|
{
|
|
title: 'Bounce Rate',
|
|
key: 'bounce_rate',
|
|
unit: '%',
|
|
inverted: true,
|
|
},
|
|
{
|
|
title: 'Session Duration',
|
|
key: 'avg_session_duration',
|
|
unit: 'min',
|
|
inverted: false,
|
|
},
|
|
] as const;
|
|
|
|
export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
|
const { range, interval, metric, setMetric, startDate, endDate } =
|
|
useOverviewOptions();
|
|
const [filters] = useEventQueryFilters();
|
|
const trpc = useTRPC();
|
|
|
|
const activeMetric = TITLES[metric]!;
|
|
const overviewQuery = useQuery(
|
|
trpc.overview.stats.queryOptions({
|
|
projectId,
|
|
range,
|
|
interval,
|
|
filters,
|
|
startDate,
|
|
endDate,
|
|
}),
|
|
);
|
|
|
|
const data =
|
|
overviewQuery.data?.series?.map((item) => ({
|
|
...item,
|
|
timestamp: new Date(item.date).getTime(),
|
|
})) || [];
|
|
|
|
return (
|
|
<>
|
|
<div className="relative -top-0.5 col-span-6 -m-4 mb-0 mt-0 md:m-0">
|
|
<div className="card mb-2 grid grid-cols-4 overflow-hidden rounded-md">
|
|
{TITLES.map((title, index) => (
|
|
<OverviewMetricCard
|
|
key={title.key}
|
|
id={title.key}
|
|
onClick={() => setMetric(index)}
|
|
label={title.title}
|
|
metric={{
|
|
current: overviewQuery.data?.metrics[title.key] ?? 0,
|
|
previous: overviewQuery.data?.metrics[`prev_${title.key}`],
|
|
}}
|
|
unit={title.unit}
|
|
data={data.map((item) => ({
|
|
current: item[title.key],
|
|
previous: item[`prev_${title.key}`],
|
|
}))}
|
|
active={metric === index}
|
|
isLoading={overviewQuery.isLoading}
|
|
/>
|
|
))}
|
|
|
|
<div
|
|
className={cn(
|
|
'col-span-4 min-h-16 flex-1 p-4 pb-0 shadow-[0_0_0_0.5px] shadow-border max-md:row-start-1 md:col-span-2',
|
|
)}
|
|
>
|
|
<OverviewLiveHistogram projectId={projectId} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card p-4">
|
|
<div className="text-center mb-3 -mt-1 text-sm font-medium text-muted-foreground">
|
|
{activeMetric.title}
|
|
</div>
|
|
<div className="w-full h-[150px]">
|
|
{overviewQuery.isLoading && <Skeleton className="h-full w-full" />}
|
|
<Chart
|
|
activeMetric={activeMetric}
|
|
interval={interval}
|
|
data={data}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const { Tooltip, TooltipProvider } = createChartTooltip<
|
|
RouterOutputs['overview']['stats']['series'][number],
|
|
{
|
|
metric: (typeof TITLES)[number];
|
|
interval: IInterval;
|
|
}
|
|
>(({ context: { metric, interval }, data: dataArray }) => {
|
|
const data = dataArray[0];
|
|
const formatDate = useFormatDateInterval(interval);
|
|
const number = useNumber();
|
|
|
|
if (!data) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="flex justify-between gap-8 text-muted-foreground">
|
|
<div>{formatDate(new Date(data.date))}</div>
|
|
</div>
|
|
<React.Fragment>
|
|
<div className="flex gap-2">
|
|
<div
|
|
className="w-[3px] rounded-full"
|
|
style={{ background: getChartColor(0) }}
|
|
/>
|
|
<div className="col flex-1 gap-1">
|
|
<div className="flex items-center gap-1">{metric.title}</div>
|
|
<div className="flex justify-between gap-8 font-mono font-medium">
|
|
<div className="row gap-1">
|
|
{number.formatWithUnit(data[metric.key])}
|
|
{!!data[`prev_${metric.key}`] && (
|
|
<span className="text-muted-foreground">
|
|
({number.formatWithUnit(data[`prev_${metric.key}`])})
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<PreviousDiffIndicatorPure
|
|
{...getPreviousMetric(
|
|
data[metric.key],
|
|
data[`prev_${metric.key}`],
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</React.Fragment>
|
|
</>
|
|
);
|
|
});
|
|
|
|
function Chart({
|
|
activeMetric,
|
|
interval,
|
|
data,
|
|
}: {
|
|
activeMetric: (typeof TITLES)[number];
|
|
interval: IInterval;
|
|
data: RouterOutputs['overview']['stats']['series'];
|
|
}) {
|
|
const xAxisProps = useXAxisProps({ interval });
|
|
const yAxisProps = useYAxisProps();
|
|
const [activeBar, setActiveBar] = useState(-1);
|
|
|
|
return (
|
|
<TooltipProvider metric={activeMetric} interval={interval}>
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<ComposedChart
|
|
data={data}
|
|
margin={{ top: 0, right: 0, left: 0, bottom: 10 }}
|
|
onMouseMove={(e) => {
|
|
setActiveBar(e.activeTooltipIndex ?? -1);
|
|
}}
|
|
barCategoryGap={2}
|
|
>
|
|
<Tooltip
|
|
cursor={{
|
|
stroke: 'var(--def-200)',
|
|
fill: 'var(--def-200)',
|
|
}}
|
|
/>
|
|
<YAxis
|
|
{...yAxisProps}
|
|
domain={[0, activeMetric.key === 'bounce_rate' ? 100 : 'auto']}
|
|
width={25}
|
|
/>
|
|
<XAxis {...omit(['scale', 'type'], xAxisProps)} />
|
|
|
|
<CartesianGrid
|
|
strokeDasharray="3 3"
|
|
horizontal={true}
|
|
vertical={false}
|
|
className="stroke-border"
|
|
/>
|
|
|
|
<Line
|
|
key={`prev_${activeMetric.key}`}
|
|
type="step"
|
|
dataKey={`prev_${activeMetric.key}`}
|
|
stroke={'var(--border)'}
|
|
strokeWidth={2}
|
|
isAnimationActive={false}
|
|
dot={false}
|
|
activeDot={{
|
|
stroke: 'var(--foreground)',
|
|
fill: 'var(--def-100)',
|
|
strokeWidth: 1,
|
|
r: 2,
|
|
}}
|
|
/>
|
|
<Bar
|
|
key={activeMetric.key}
|
|
dataKey={activeMetric.key}
|
|
isAnimationActive={false}
|
|
shape={(props: any) => (
|
|
<BarShapeBlue isActive={activeBar === props.index} {...props} />
|
|
)}
|
|
/>
|
|
</ComposedChart>
|
|
</ResponsiveContainer>
|
|
</TooltipProvider>
|
|
);
|
|
}
|