improve(funnel): revert the revert of the ui (api still reverted)
This commit is contained in:
@@ -29,7 +29,7 @@ interface PreviousDiffIndicatorProps {
|
||||
children?: React.ReactNode;
|
||||
inverted?: boolean;
|
||||
className?: string;
|
||||
size?: 'sm' | 'lg';
|
||||
size?: 'sm' | 'lg' | 'md';
|
||||
}
|
||||
|
||||
export function PreviousDiffIndicator({
|
||||
@@ -80,6 +80,7 @@ export function PreviousDiffIndicator({
|
||||
'flex size-4 items-center justify-center rounded-full',
|
||||
variant,
|
||||
size === 'lg' && 'size-8',
|
||||
size === 'md' && 'size-6',
|
||||
)}
|
||||
>
|
||||
{renderIcon()}
|
||||
|
||||
@@ -39,15 +39,10 @@ export function Chart({
|
||||
const { isEditMode } = useReportChartContext();
|
||||
const mostDropoffs = findMostDropoffs(steps);
|
||||
const lastStep = last(steps)!;
|
||||
const prevLastStep = last(previous.steps);
|
||||
const prevLastStep = previous?.steps ? last(previous.steps) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-4 @container',
|
||||
isEditMode ? 'card' : '-m-4',
|
||||
)}
|
||||
>
|
||||
<div className={cn('col gap-4 @container', isEditMode ? 'card' : '-m-4')}>
|
||||
<div
|
||||
className={cn(
|
||||
'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 flex-1 items-center gap-8">
|
||||
<div className="flex flex-1 items-center gap-8 min-w-0">
|
||||
<MetricCardNumber
|
||||
label="Converted"
|
||||
value={lastStep.count}
|
||||
enhancer={
|
||||
<PreviousDiffIndicator
|
||||
size="lg"
|
||||
size="md"
|
||||
{...getPreviousMetric(lastStep.count, prevLastStep?.count)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<MetricCardNumber
|
||||
label="Percent"
|
||||
value={`${totalSessions ? round((lastStep.count / totalSessions) * 100, 1) : 0}%`}
|
||||
value={`${totalSessions ? round((lastStep.count / totalSessions) * 100, 2) : 0}%`}
|
||||
enhancer={
|
||||
<PreviousDiffIndicator
|
||||
size="lg"
|
||||
size="md"
|
||||
{...getPreviousMetric(lastStep.count, prevLastStep?.count)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<MetricCardNumber
|
||||
className="flex-1"
|
||||
label="Most dropoffs"
|
||||
value={mostDropoffs.event.displayName}
|
||||
enhancer={
|
||||
<PreviousDiffIndicator
|
||||
size="lg"
|
||||
size="md"
|
||||
{...getPreviousMetric(lastStep.count, prevLastStep?.count)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</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 className="flex flex-col divide-y divide-def-200">
|
||||
<div className="col divide-y divide-def-200">
|
||||
{steps.map((step, index) => {
|
||||
const percent = (step.count / totalSessions) * 100;
|
||||
const isMostDropoffs = mostDropoffs.event.id === step.event.id;
|
||||
return (
|
||||
<div
|
||||
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">
|
||||
<ColorSquare className="absolute left-0 top-0.5">
|
||||
@@ -123,27 +101,27 @@ export function Chart({
|
||||
<div className="font-semibold mt-1">
|
||||
{step.event.displayName}
|
||||
</div>
|
||||
<div className="flex items-center gap-8 text-sm">
|
||||
<div className="grid grid-cols-4 max-w-lg gap-8 text-sm">
|
||||
<TooltipComplete
|
||||
disabled={!previous.steps[index]}
|
||||
disabled={!previous?.steps?.[index]}
|
||||
content={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>
|
||||
Last period:{' '}
|
||||
<span className="font-mono">
|
||||
{previous.steps[index]?.previousCount}
|
||||
{previous?.steps?.[index]?.previousCount}
|
||||
</span>
|
||||
</span>
|
||||
<PreviousDiffIndicator
|
||||
{...getPreviousMetric(
|
||||
step.previousCount,
|
||||
previous.steps[index]?.previousCount,
|
||||
previous?.steps?.[index]?.previousCount,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="col gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Total:
|
||||
</span>
|
||||
@@ -155,26 +133,26 @@ export function Chart({
|
||||
</div>
|
||||
</TooltipComplete>
|
||||
<TooltipComplete
|
||||
disabled={!previous.steps[index]}
|
||||
disabled={!previous?.steps?.[index]}
|
||||
content={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>
|
||||
Last period:{' '}
|
||||
<span className="font-mono">
|
||||
{previous.steps[index]?.dropoffCount}
|
||||
{previous?.steps?.[index]?.dropoffCount}
|
||||
</span>
|
||||
</span>
|
||||
<PreviousDiffIndicator
|
||||
inverted
|
||||
{...getPreviousMetric(
|
||||
step.dropoffCount,
|
||||
previous.steps[index]?.dropoffCount,
|
||||
previous?.steps?.[index]?.dropoffCount,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="col gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Dropoff:
|
||||
</span>
|
||||
@@ -192,25 +170,25 @@ export function Chart({
|
||||
</div>
|
||||
</TooltipComplete>
|
||||
<TooltipComplete
|
||||
disabled={!previous.steps[index]}
|
||||
disabled={!previous?.steps?.[index]}
|
||||
content={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>
|
||||
Last period:{' '}
|
||||
<span className="font-mono">
|
||||
{previous.steps[index]?.count}
|
||||
{previous?.steps?.[index]?.count}
|
||||
</span>
|
||||
</span>
|
||||
<PreviousDiffIndicator
|
||||
{...getPreviousMetric(
|
||||
step.count,
|
||||
previous.steps[index]?.count,
|
||||
previous?.steps?.[index]?.count,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="col gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Current:
|
||||
</span>
|
||||
@@ -219,12 +197,42 @@ export function Chart({
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<Progress
|
||||
size="lg"
|
||||
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(
|
||||
'bg-primary',
|
||||
|
||||
@@ -13,7 +13,16 @@ import { Chart } from './chart';
|
||||
|
||||
export function ReportFunnelChart() {
|
||||
const {
|
||||
report: { events, range, projectId, funnelWindow, funnelGroup },
|
||||
report: {
|
||||
events,
|
||||
range,
|
||||
projectId,
|
||||
funnelWindow,
|
||||
funnelGroup,
|
||||
startDate,
|
||||
endDate,
|
||||
previous,
|
||||
},
|
||||
isLazyLoading,
|
||||
} = useReportChartContext();
|
||||
|
||||
@@ -26,8 +35,10 @@ export function ReportFunnelChart() {
|
||||
breakdowns: [],
|
||||
funnelWindow,
|
||||
funnelGroup,
|
||||
previous: false,
|
||||
previous,
|
||||
metric: 'sum',
|
||||
startDate,
|
||||
endDate,
|
||||
};
|
||||
const res = api.chart.funnel.useQuery(input, {
|
||||
keepPreviousData: true,
|
||||
|
||||
@@ -122,19 +122,21 @@ export function MetricCardNumber({
|
||||
label,
|
||||
value,
|
||||
enhancer,
|
||||
className,
|
||||
}: {
|
||||
label: React.ReactNode;
|
||||
value: React.ReactNode;
|
||||
enhancer?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
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 min-w-0 items-center gap-2 text-left">
|
||||
<span className="truncate text-muted-foreground">{label}</span>
|
||||
</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>
|
||||
{enhancer}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,32 @@
|
||||
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 { 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';
|
||||
|
||||
interface ReportChartTypeProps {
|
||||
@@ -13,20 +35,57 @@ interface ReportChartTypeProps {
|
||||
export function ReportChartType({ className }: ReportChartTypeProps) {
|
||||
const dispatch = useDispatch();
|
||||
const type = useSelector((state) => state.report.chartType);
|
||||
const items = objectToZodEnums(chartTypes).map((key) => ({
|
||||
label: chartTypes[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 (
|
||||
<Combobox
|
||||
icon={LineChartIcon}
|
||||
className={className}
|
||||
placeholder="Chart type"
|
||||
onChange={(value) => {
|
||||
dispatch(changeChartType(value));
|
||||
}}
|
||||
value={type}
|
||||
items={objectToZodEnums(chartTypes).map((key) => ({
|
||||
label: chartTypes[key],
|
||||
value: key,
|
||||
}))}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TooltipPortal } from '@radix-ui/react-tooltip';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||
|
||||
interface TooltipCompleteProps {
|
||||
@@ -15,12 +16,17 @@ export function TooltipComplete({
|
||||
}: TooltipCompleteProps) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={typeof children !== 'string'}>
|
||||
<TooltipTrigger
|
||||
className="appearance-none"
|
||||
style={{ textAlign: 'inherit' }}
|
||||
>
|
||||
{children}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={side} disabled={disabled}>
|
||||
{content}
|
||||
</TooltipContent>
|
||||
<TooltipPortal>
|
||||
<TooltipContent side={side} disabled={disabled}>
|
||||
{content}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,11 +30,7 @@ const Progress = React.forwardRef<
|
||||
}}
|
||||
/>
|
||||
{value && size !== 'sm' && (
|
||||
<div
|
||||
className={
|
||||
'z-5 absolute bottom-0 top-0 flex items-center px-2 text-sm font-medium font-mono'
|
||||
}
|
||||
>
|
||||
<div 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>
|
||||
)}
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
getChartPrevStartEndDate,
|
||||
getChartStartEndDate,
|
||||
getFunnelData,
|
||||
getFunnelStep,
|
||||
} from './chart.helpers';
|
||||
|
||||
function utc(date: string | Date) {
|
||||
@@ -87,9 +86,12 @@ export const chartRouter = createTRPCRouter({
|
||||
.map((item) => item.replace(/\.([0-9]+)/g, '[*]'))
|
||||
.map((item) => `properties.${item}`);
|
||||
|
||||
if (event === '*') {
|
||||
properties.push('name');
|
||||
}
|
||||
|
||||
properties.push(
|
||||
'has_profile',
|
||||
'name',
|
||||
'path',
|
||||
'origin',
|
||||
'referrer',
|
||||
@@ -184,7 +186,9 @@ export const chartRouter = createTRPCRouter({
|
||||
|
||||
const [current, previous] = await Promise.all([
|
||||
getFunnelData({ ...input, ...currentPeriod }),
|
||||
getFunnelData({ ...input, ...previousPeriod }),
|
||||
input.previous
|
||||
? getFunnelData({ ...input, ...previousPeriod })
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
|
||||
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 }) => {
|
||||
if (ctx.session.userId) {
|
||||
const access = await getProjectAccessCached({
|
||||
|
||||
Reference in New Issue
Block a user