improve(funnel): revert the revert of the ui (api still reverted)

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-11-23 23:55:11 +01:00
parent b0c8e25b0a
commit 222b54b907
8 changed files with 165 additions and 89 deletions

View File

@@ -29,7 +29,7 @@ interface PreviousDiffIndicatorProps {
children?: React.ReactNode; children?: React.ReactNode;
inverted?: boolean; inverted?: boolean;
className?: string; className?: string;
size?: 'sm' | 'lg'; size?: 'sm' | 'lg' | 'md';
} }
export function PreviousDiffIndicator({ export function PreviousDiffIndicator({
@@ -80,6 +80,7 @@ export function PreviousDiffIndicator({
'flex size-4 items-center justify-center rounded-full', 'flex size-4 items-center justify-center rounded-full',
variant, variant,
size === 'lg' && 'size-8', size === 'lg' && 'size-8',
size === 'md' && 'size-6',
)} )}
> >
{renderIcon()} {renderIcon()}

View File

@@ -39,15 +39,10 @@ export function Chart({
const { isEditMode } = useReportChartContext(); const { isEditMode } = useReportChartContext();
const mostDropoffs = findMostDropoffs(steps); const mostDropoffs = findMostDropoffs(steps);
const lastStep = last(steps)!; const lastStep = last(steps)!;
const prevLastStep = last(previous.steps); const prevLastStep = previous?.steps ? last(previous.steps) : null;
return ( return (
<div <div className={cn('col gap-4 @container', isEditMode ? 'card' : '-m-4')}>
className={cn(
'flex flex-col gap-4 @container',
isEditMode ? 'card' : '-m-4',
)}
>
<div <div
className={cn( className={cn(
'border-b border-border bg-def-100', 'border-b border-border bg-def-100',
@@ -55,66 +50,49 @@ export function Chart({
)} )}
> >
<div className="flex items-center gap-8 p-4 px-8"> <div className="flex items-center gap-8 p-4 px-8">
<div className="flex flex-1 items-center gap-8"> <div className="flex flex-1 items-center gap-8 min-w-0">
<MetricCardNumber <MetricCardNumber
label="Converted" label="Converted"
value={lastStep.count} value={lastStep.count}
enhancer={ enhancer={
<PreviousDiffIndicator <PreviousDiffIndicator
size="lg" size="md"
{...getPreviousMetric(lastStep.count, prevLastStep?.count)} {...getPreviousMetric(lastStep.count, prevLastStep?.count)}
/> />
} }
/> />
<MetricCardNumber <MetricCardNumber
label="Percent" label="Percent"
value={`${totalSessions ? round((lastStep.count / totalSessions) * 100, 1) : 0}%`} value={`${totalSessions ? round((lastStep.count / totalSessions) * 100, 2) : 0}%`}
enhancer={ enhancer={
<PreviousDiffIndicator <PreviousDiffIndicator
size="lg" size="md"
{...getPreviousMetric(lastStep.count, prevLastStep?.count)} {...getPreviousMetric(lastStep.count, prevLastStep?.count)}
/> />
} }
/> />
<MetricCardNumber <MetricCardNumber
className="flex-1"
label="Most dropoffs" label="Most dropoffs"
value={mostDropoffs.event.displayName} value={mostDropoffs.event.displayName}
enhancer={ enhancer={
<PreviousDiffIndicator <PreviousDiffIndicator
size="lg" size="md"
{...getPreviousMetric(lastStep.count, prevLastStep?.count)} {...getPreviousMetric(lastStep.count, prevLastStep?.count)}
/> />
} }
/> />
</div> </div>
<div className="hidden shrink-0 gap-2 @xl:flex">
{steps.map((step) => {
return (
<div
className="flex h-16 w-4 items-end overflow-hidden rounded bg-def-200"
key={step.event.id}
>
<div
className={cn(
'bg-foreground w-full',
step.event.id === mostDropoffs.event.id && 'bg-rose-500',
)}
style={{ height: `${step.percent}%` }}
/>
</div>
);
})}
</div> </div>
</div> </div>
</div> <div className="col divide-y divide-def-200">
<div className="flex flex-col divide-y divide-def-200">
{steps.map((step, index) => { {steps.map((step, index) => {
const percent = (step.count / totalSessions) * 100; const percent = (step.count / totalSessions) * 100;
const isMostDropoffs = mostDropoffs.event.id === step.event.id; const isMostDropoffs = mostDropoffs.event.id === step.event.id;
return ( return (
<div <div
key={step.event.id} key={step.event.id}
className="flex flex-col gap-4 px-4 py-4 @2xl:flex-row @2xl:px-8" className="col gap-12 px-4 py-4 @2xl:flex-row @2xl:px-8"
> >
<div className="relative flex flex-1 flex-col gap-2 pl-8"> <div className="relative flex flex-1 flex-col gap-2 pl-8">
<ColorSquare className="absolute left-0 top-0.5"> <ColorSquare className="absolute left-0 top-0.5">
@@ -123,27 +101,27 @@ export function Chart({
<div className="font-semibold mt-1"> <div className="font-semibold mt-1">
{step.event.displayName} {step.event.displayName}
</div> </div>
<div className="flex items-center gap-8 text-sm"> <div className="grid grid-cols-4 max-w-lg gap-8 text-sm">
<TooltipComplete <TooltipComplete
disabled={!previous.steps[index]} disabled={!previous?.steps?.[index]}
content={ content={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span> <span>
Last period:{' '} Last period:{' '}
<span className="font-mono"> <span className="font-mono">
{previous.steps[index]?.previousCount} {previous?.steps?.[index]?.previousCount}
</span> </span>
</span> </span>
<PreviousDiffIndicator <PreviousDiffIndicator
{...getPreviousMetric( {...getPreviousMetric(
step.previousCount, step.previousCount,
previous.steps[index]?.previousCount, previous?.steps?.[index]?.previousCount,
)} )}
/> />
</div> </div>
} }
> >
<div className="flex flex-col gap-2"> <div className="col gap-2">
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
Total: Total:
</span> </span>
@@ -155,26 +133,26 @@ export function Chart({
</div> </div>
</TooltipComplete> </TooltipComplete>
<TooltipComplete <TooltipComplete
disabled={!previous.steps[index]} disabled={!previous?.steps?.[index]}
content={ content={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span> <span>
Last period:{' '} Last period:{' '}
<span className="font-mono"> <span className="font-mono">
{previous.steps[index]?.dropoffCount} {previous?.steps?.[index]?.dropoffCount}
</span> </span>
</span> </span>
<PreviousDiffIndicator <PreviousDiffIndicator
inverted inverted
{...getPreviousMetric( {...getPreviousMetric(
step.dropoffCount, step.dropoffCount,
previous.steps[index]?.dropoffCount, previous?.steps?.[index]?.dropoffCount,
)} )}
/> />
</div> </div>
} }
> >
<div className="flex flex-col gap-2"> <div className="col gap-2">
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
Dropoff: Dropoff:
</span> </span>
@@ -192,25 +170,25 @@ export function Chart({
</div> </div>
</TooltipComplete> </TooltipComplete>
<TooltipComplete <TooltipComplete
disabled={!previous.steps[index]} disabled={!previous?.steps?.[index]}
content={ content={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span> <span>
Last period:{' '} Last period:{' '}
<span className="font-mono"> <span className="font-mono">
{previous.steps[index]?.count} {previous?.steps?.[index]?.count}
</span> </span>
</span> </span>
<PreviousDiffIndicator <PreviousDiffIndicator
{...getPreviousMetric( {...getPreviousMetric(
step.count, step.count,
previous.steps[index]?.count, previous?.steps?.[index]?.count,
)} )}
/> />
</div> </div>
} }
> >
<div className="flex flex-col gap-2"> <div className="col gap-2">
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
Current: Current:
</span> </span>
@@ -219,12 +197,42 @@ export function Chart({
</div> </div>
</div> </div>
</TooltipComplete> </TooltipComplete>
<TooltipComplete
disabled={!previous?.steps?.[index]}
content={
<div className="flex items-center gap-2">
<span>
Last period:{' '}
<span className="font-mono">
{previous?.steps?.[index]?.count}
</span>
</span>
<PreviousDiffIndicator
{...getPreviousMetric(
step.count,
previous?.steps?.[index]?.count,
)}
/>
</div>
}
>
<div className="col gap-2">
<span className="text-xs text-muted-foreground">
Percent:
</span>
<div className="flex items-center gap-4">
<span className="text-lg font-mono">
{Number.isNaN(percent) ? 0 : round(percent, 2)}%
</span>
</div>
</div>
</TooltipComplete>
</div> </div>
</div> </div>
<Progress <Progress
size="lg" size="lg"
className={cn( className={cn(
'w-full @2xl:w-1/2 text-white bg-def-200 mt-0.5 dark:text-black', 'w-full @2xl:w-1/4 text-white bg-def-200 mt-0.5 dark:text-black',
)} )}
innerClassName={cn( innerClassName={cn(
'bg-primary', 'bg-primary',

View File

@@ -13,7 +13,16 @@ import { Chart } from './chart';
export function ReportFunnelChart() { export function ReportFunnelChart() {
const { const {
report: { events, range, projectId, funnelWindow, funnelGroup }, report: {
events,
range,
projectId,
funnelWindow,
funnelGroup,
startDate,
endDate,
previous,
},
isLazyLoading, isLazyLoading,
} = useReportChartContext(); } = useReportChartContext();
@@ -26,8 +35,10 @@ export function ReportFunnelChart() {
breakdowns: [], breakdowns: [],
funnelWindow, funnelWindow,
funnelGroup, funnelGroup,
previous: false, previous,
metric: 'sum', metric: 'sum',
startDate,
endDate,
}; };
const res = api.chart.funnel.useQuery(input, { const res = api.chart.funnel.useQuery(input, {
keepPreviousData: true, keepPreviousData: true,

View File

@@ -122,19 +122,21 @@ export function MetricCardNumber({
label, label,
value, value,
enhancer, enhancer,
className,
}: { }: {
label: React.ReactNode; label: React.ReactNode;
value: React.ReactNode; value: React.ReactNode;
enhancer?: React.ReactNode; enhancer?: React.ReactNode;
className?: string;
}) { }) {
return ( return (
<div className="flex min-w-0 flex-col gap-2"> <div className={cn('flex min-w-0 flex-col gap-2', className)}>
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2 text-left"> <div className="flex min-w-0 items-center gap-2 text-left">
<span className="truncate text-muted-foreground">{label}</span> <span className="truncate text-muted-foreground">{label}</span>
</div> </div>
</div> </div>
<div className="flex items-end justify-between"> <div className="flex items-end justify-between gap-4">
<div className="truncate font-mono text-3xl font-bold">{value}</div> <div className="truncate font-mono text-3xl font-bold">{value}</div>
{enhancer} {enhancer}
</div> </div>

View File

@@ -1,10 +1,32 @@
import { useDispatch, useSelector } from '@/redux'; import { useDispatch, useSelector } from '@/redux';
import { LineChartIcon } from 'lucide-react'; import {
AreaChartIcon,
ChartBarIcon,
ChartColumnIncreasingIcon,
ConeIcon,
GaugeIcon,
Globe2Icon,
LineChartIcon,
type LucideIcon,
PieChartIcon,
UsersIcon,
} from 'lucide-react';
import { chartTypes } from '@openpanel/constants'; import { chartTypes } from '@openpanel/constants';
import { objectToZodEnums } from '@openpanel/validation'; import { objectToZodEnums } from '@openpanel/validation';
import { Combobox } from '../ui/combobox'; import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { cn } from '@/utils/cn';
import { Button } from '../ui/button';
import { changeChartType } from './reportSlice'; import { changeChartType } from './reportSlice';
interface ReportChartTypeProps { interface ReportChartTypeProps {
@@ -13,20 +35,57 @@ interface ReportChartTypeProps {
export function ReportChartType({ className }: ReportChartTypeProps) { export function ReportChartType({ className }: ReportChartTypeProps) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const type = useSelector((state) => state.report.chartType); const type = useSelector((state) => state.report.chartType);
const items = objectToZodEnums(chartTypes).map((key) => ({
return (
<Combobox
icon={LineChartIcon}
className={className}
placeholder="Chart type"
onChange={(value) => {
dispatch(changeChartType(value));
}}
value={type}
items={objectToZodEnums(chartTypes).map((key) => ({
label: chartTypes[key], label: chartTypes[key],
value: key, value: key,
}))} }));
/>
const Icons: Record<keyof typeof chartTypes, LucideIcon> = {
area: AreaChartIcon,
bar: ChartBarIcon,
pie: PieChartIcon,
funnel: ((props) => (
<ConeIcon className={cn('rotate-180', props.className)} />
)) as LucideIcon,
histogram: ChartColumnIncreasingIcon,
linear: LineChartIcon,
metric: GaugeIcon,
retention: UsersIcon,
map: Globe2Icon,
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
icon={Icons[type]}
className={cn('justify-start', className)}
>
{items.find((item) => item.value === type)?.label}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>Available charts</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
{items.map((item) => {
const Icon = Icons[item.value];
return (
<DropdownMenuItem
key={item.value}
onClick={() => dispatch(changeChartType(item.value))}
>
{item.label}
<DropdownMenuShortcut>
<Icon className="size-4" />
</DropdownMenuShortcut>
</DropdownMenuItem>
);
})}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
); );
} }

View File

@@ -1,3 +1,4 @@
import { TooltipPortal } from '@radix-ui/react-tooltip';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
interface TooltipCompleteProps { interface TooltipCompleteProps {
@@ -15,12 +16,17 @@ export function TooltipComplete({
}: TooltipCompleteProps) { }: TooltipCompleteProps) {
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger asChild={typeof children !== 'string'}> <TooltipTrigger
className="appearance-none"
style={{ textAlign: 'inherit' }}
>
{children} {children}
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal>
<TooltipContent side={side} disabled={disabled}> <TooltipContent side={side} disabled={disabled}>
{content} {content}
</TooltipContent> </TooltipContent>
</TooltipPortal>
</Tooltip> </Tooltip>
); );
} }

View File

@@ -30,11 +30,7 @@ const Progress = React.forwardRef<
}} }}
/> />
{value && size !== 'sm' && ( {value && size !== 'sm' && (
<div <div className="z-5 absolute bottom-0 top-0 flex items-center px-2 text-sm font-medium font-mono">
className={
'z-5 absolute bottom-0 top-0 flex items-center px-2 text-sm font-medium font-mono'
}
>
<div>{round(value, 2)}%</div> <div>{round(value, 2)}%</div>
</div> </div>
)} )}

View File

@@ -32,7 +32,6 @@ import {
getChartPrevStartEndDate, getChartPrevStartEndDate,
getChartStartEndDate, getChartStartEndDate,
getFunnelData, getFunnelData,
getFunnelStep,
} from './chart.helpers'; } from './chart.helpers';
function utc(date: string | Date) { function utc(date: string | Date) {
@@ -87,9 +86,12 @@ export const chartRouter = createTRPCRouter({
.map((item) => item.replace(/\.([0-9]+)/g, '[*]')) .map((item) => item.replace(/\.([0-9]+)/g, '[*]'))
.map((item) => `properties.${item}`); .map((item) => `properties.${item}`);
if (event === '*') {
properties.push('name');
}
properties.push( properties.push(
'has_profile', 'has_profile',
'name',
'path', 'path',
'origin', 'origin',
'referrer', 'referrer',
@@ -184,7 +186,9 @@ export const chartRouter = createTRPCRouter({
const [current, previous] = await Promise.all([ const [current, previous] = await Promise.all([
getFunnelData({ ...input, ...currentPeriod }), getFunnelData({ ...input, ...currentPeriod }),
getFunnelData({ ...input, ...previousPeriod }), input.previous
? getFunnelData({ ...input, ...previousPeriod })
: Promise.resolve(null),
]); ]);
return { return {
@@ -193,17 +197,6 @@ export const chartRouter = createTRPCRouter({
}; };
}), }),
funnelStep: protectedProcedure
.input(
zChartInput.extend({
step: z.number(),
}),
)
.query(async ({ input }) => {
const currentPeriod = getChartStartEndDate(input);
return getFunnelStep({ ...input, ...currentPeriod });
}),
chart: publicProcedure.input(zChartInput).query(async ({ input, ctx }) => { chart: publicProcedure.input(zChartInput).query(async ({ input, ctx }) => {
if (ctx.session.userId) { if (ctx.session.userId) {
const access = await getProjectAccessCached({ const access = await getProjectAccessCached({