fix(api): filter empty values when using sum and average, added min and max
This commit is contained in:
@@ -81,7 +81,7 @@
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"lottie-react": "^2.4.0",
|
||||
"lucide-react": "^0.451.0",
|
||||
"lucide-react": "^0.513.0",
|
||||
"mathjs": "^12.3.2",
|
||||
"mitt": "^3.0.1",
|
||||
"next": "14.2.1",
|
||||
|
||||
92
apps/dashboard/src/components/report/ReportSegment.tsx
Normal file
92
apps/dashboard/src/components/report/ReportSegment.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
ActivityIcon,
|
||||
ClockIcon,
|
||||
EqualApproximatelyIcon,
|
||||
type LucideIcon,
|
||||
SigmaIcon,
|
||||
TrendingDownIcon,
|
||||
TrendingUpIcon,
|
||||
UserCheck2Icon,
|
||||
UserCheckIcon,
|
||||
UsersIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { chartSegments } from '@openpanel/constants';
|
||||
import { type IChartEventSegment, mapKeys } from '@openpanel/validation';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { Button } from '../ui/button';
|
||||
|
||||
interface ReportChartTypeProps {
|
||||
className?: string;
|
||||
value: IChartEventSegment;
|
||||
onChange: (segment: IChartEventSegment) => void;
|
||||
}
|
||||
export function ReportSegment({
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
}: ReportChartTypeProps) {
|
||||
const items = mapKeys(chartSegments).map((key) => ({
|
||||
label: chartSegments[key],
|
||||
value: key,
|
||||
}));
|
||||
|
||||
const Icons: Record<IChartEventSegment, LucideIcon> = {
|
||||
event: ActivityIcon,
|
||||
user: UsersIcon,
|
||||
session: ClockIcon,
|
||||
user_average: UserCheck2Icon,
|
||||
one_event_per_user: UserCheckIcon,
|
||||
property_sum: SigmaIcon,
|
||||
property_average: EqualApproximatelyIcon,
|
||||
property_max: TrendingUpIcon,
|
||||
property_min: TrendingDownIcon,
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
icon={Icons[value]}
|
||||
className={cn('justify-start', className)}
|
||||
>
|
||||
{items.find((item) => item.value === value)?.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={() => onChange(item.value)}
|
||||
className="group"
|
||||
>
|
||||
{item.label}
|
||||
<DropdownMenuShortcut>
|
||||
<Icon className="size-4 group-hover:text-blue-500 group-hover:scale-125 transition-all group-hover:rotate-12" />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
import { ColorSquare } from '@/components/color-square';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
||||
import { DropdownMenuComposed } from '@/components/ui/dropdown-menu';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useDebounceFn } from '@/hooks/useDebounceFn';
|
||||
@@ -28,7 +27,8 @@ import { CSS } from '@dnd-kit/utilities';
|
||||
import { shortId } from '@openpanel/common';
|
||||
import { alphabetIds } from '@openpanel/constants';
|
||||
import type { IChartEvent } from '@openpanel/validation';
|
||||
import { FilterIcon, GanttChartIcon, HandIcon, Users } from 'lucide-react';
|
||||
import { FilterIcon, GanttChartIcon, HandIcon } from 'lucide-react';
|
||||
import { ReportSegment } from '../ReportSegment';
|
||||
import {
|
||||
addEvent,
|
||||
changeEvent,
|
||||
@@ -82,7 +82,8 @@ function SortableEvent({
|
||||
{(showSegment || showAddFilter) && (
|
||||
<div className="flex gap-2 p-2 pt-0">
|
||||
{showSegment && (
|
||||
<DropdownMenuComposed
|
||||
<ReportSegment
|
||||
value={event.segment}
|
||||
onChange={(segment) => {
|
||||
dispatch(
|
||||
changeEvent({
|
||||
@@ -91,73 +92,7 @@ function SortableEvent({
|
||||
}),
|
||||
);
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
value: 'event',
|
||||
label: 'All events',
|
||||
},
|
||||
{
|
||||
value: 'user',
|
||||
label: 'Unique users',
|
||||
},
|
||||
{
|
||||
value: 'session',
|
||||
label: 'Unique sessions',
|
||||
},
|
||||
{
|
||||
value: 'user_average',
|
||||
label: 'Average event per user',
|
||||
},
|
||||
{
|
||||
value: 'one_event_per_user',
|
||||
label: 'One event per user',
|
||||
},
|
||||
{
|
||||
value: 'property_sum',
|
||||
label: 'Sum of property',
|
||||
},
|
||||
{
|
||||
value: 'property_average',
|
||||
label: 'Average of property',
|
||||
},
|
||||
]}
|
||||
label="Segment"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded-md border border-border bg-card p-1 px-2 text-sm font-medium leading-none"
|
||||
>
|
||||
{event.segment === 'user' ? (
|
||||
<>
|
||||
<Users size={12} /> Unique users
|
||||
</>
|
||||
) : event.segment === 'session' ? (
|
||||
<>
|
||||
<Users size={12} /> Unique sessions
|
||||
</>
|
||||
) : event.segment === 'user_average' ? (
|
||||
<>
|
||||
<Users size={12} /> Average event per user
|
||||
</>
|
||||
) : event.segment === 'one_event_per_user' ? (
|
||||
<>
|
||||
<Users size={12} /> One event per user
|
||||
</>
|
||||
) : event.segment === 'property_sum' ? (
|
||||
<>
|
||||
<Users size={12} /> Sum of property
|
||||
</>
|
||||
) : event.segment === 'property_average' ? (
|
||||
<>
|
||||
<Users size={12} /> Average of property
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<GanttChartIcon size={12} /> All events
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</DropdownMenuComposed>
|
||||
/>
|
||||
)}
|
||||
{showAddFilter && (
|
||||
<PropertiesCombobox
|
||||
|
||||
@@ -102,6 +102,18 @@ export const chartTypes = {
|
||||
conversion: 'Conversion',
|
||||
} as const;
|
||||
|
||||
export const chartSegments = {
|
||||
event: 'All events',
|
||||
user: 'Unique users',
|
||||
session: 'Unique sessions',
|
||||
user_average: 'Average users',
|
||||
one_event_per_user: 'One event per user',
|
||||
property_sum: 'Sum of property',
|
||||
property_average: 'Average of property',
|
||||
property_max: 'Max of property',
|
||||
property_min: 'Min of property',
|
||||
};
|
||||
|
||||
export const lineTypes = {
|
||||
monotone: 'Monotone',
|
||||
monotoneX: 'Monotone X',
|
||||
|
||||
@@ -162,12 +162,22 @@ export function getChartSql({
|
||||
|
||||
if (event.segment === 'property_sum' && event.property) {
|
||||
sb.select.count = `sum(toFloat64(${getSelectPropertyKey(event.property)})) as count`;
|
||||
sb.where.property = `${getSelectPropertyKey(event.property)} IS NOT NULL`;
|
||||
sb.where.property = `${getSelectPropertyKey(event.property)} IS NOT NULL AND notEmpty(${getSelectPropertyKey(event.property)})`;
|
||||
}
|
||||
|
||||
if (event.segment === 'property_average' && event.property) {
|
||||
sb.select.count = `avg(toFloat64(${getSelectPropertyKey(event.property)})) as count`;
|
||||
sb.where.property = `${getSelectPropertyKey(event.property)} IS NOT NULL`;
|
||||
sb.where.property = `${getSelectPropertyKey(event.property)} IS NOT NULL AND notEmpty(${getSelectPropertyKey(event.property)})`;
|
||||
}
|
||||
|
||||
if (event.segment === 'property_max' && event.property) {
|
||||
sb.select.count = `max(toFloat64(${getSelectPropertyKey(event.property)})) as count`;
|
||||
sb.where.property = `${getSelectPropertyKey(event.property)} IS NOT NULL AND notEmpty(${getSelectPropertyKey(event.property)})`;
|
||||
}
|
||||
|
||||
if (event.segment === 'property_min' && event.property) {
|
||||
sb.select.count = `min(toFloat64(${getSelectPropertyKey(event.property)})) as count`;
|
||||
sb.where.property = `${getSelectPropertyKey(event.property)} IS NOT NULL AND notEmpty(${getSelectPropertyKey(event.property)})`;
|
||||
}
|
||||
|
||||
if (event.segment === 'one_event_per_user') {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
chartSegments,
|
||||
chartTypes,
|
||||
intervals,
|
||||
lineTypes,
|
||||
@@ -29,6 +30,11 @@ export const zChartEventFilter = z.object({
|
||||
.describe('The values to filter on'),
|
||||
});
|
||||
|
||||
export const zChartEventSegment = z
|
||||
.enum(objectToZodEnums(chartSegments))
|
||||
.default('event')
|
||||
.describe('Defines how the event data should be segmented or aggregated');
|
||||
|
||||
export const zChartEvent = z.object({
|
||||
id: z
|
||||
.string()
|
||||
@@ -45,18 +51,7 @@ export const zChartEvent = z.object({
|
||||
.describe(
|
||||
'Optional property of the event used for specific segment calculations (e.g., value for property_sum/average)',
|
||||
),
|
||||
segment: z
|
||||
.enum([
|
||||
'event',
|
||||
'user',
|
||||
'session',
|
||||
'user_average',
|
||||
'one_event_per_user',
|
||||
'property_sum',
|
||||
'property_average',
|
||||
])
|
||||
.default('event')
|
||||
.describe('Defines how the event data should be segmented or aggregated'),
|
||||
segment: zChartEventSegment,
|
||||
filters: z
|
||||
.array(zChartEventFilter)
|
||||
.default([])
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { z } from 'zod';
|
||||
import type {
|
||||
zChartBreakdown,
|
||||
zChartEvent,
|
||||
zChartEventSegment,
|
||||
zChartInput,
|
||||
zChartInputAI,
|
||||
zChartType,
|
||||
@@ -23,6 +24,7 @@ export type IChartProps = z.infer<typeof zReportInput> & {
|
||||
previousIndicatorInverted?: boolean;
|
||||
};
|
||||
export type IChartEvent = z.infer<typeof zChartEvent>;
|
||||
export type IChartEventSegment = z.infer<typeof zChartEventSegment>;
|
||||
export type IChartEventFilter = IChartEvent['filters'][number];
|
||||
export type IChartEventFilterValue =
|
||||
IChartEvent['filters'][number]['value'][number];
|
||||
|
||||
61
pnpm-lock.yaml
generated
61
pnpm-lock.yaml
generated
@@ -413,8 +413,8 @@ importers:
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
lucide-react:
|
||||
specifier: ^0.451.0
|
||||
version: 0.451.0(react@18.2.0)
|
||||
specifier: ^0.513.0
|
||||
version: 0.513.0(react@18.2.0)
|
||||
mathjs:
|
||||
specifier: ^12.3.2
|
||||
version: 12.3.2
|
||||
@@ -1072,40 +1072,6 @@ importers:
|
||||
specifier: ^5.2.2
|
||||
version: 5.6.3
|
||||
|
||||
packages/fire:
|
||||
dependencies:
|
||||
'@faker-js/faker':
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1
|
||||
'@openpanel/common':
|
||||
specifier: workspace:*
|
||||
version: link:../common
|
||||
'@openpanel/db':
|
||||
specifier: workspace:*
|
||||
version: link:../db
|
||||
csv-parse:
|
||||
specifier: ^5.6.0
|
||||
version: 5.6.0
|
||||
date-fns:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1
|
||||
devDependencies:
|
||||
'@openpanel/tsconfig':
|
||||
specifier: workspace:*
|
||||
version: link:../../tooling/typescript
|
||||
'@openpanel/validation':
|
||||
specifier: workspace:*
|
||||
version: link:../validation
|
||||
'@types/node':
|
||||
specifier: 20.14.8
|
||||
version: 20.14.8
|
||||
tsup:
|
||||
specifier: ^7.2.0
|
||||
version: 7.3.0(postcss@8.5.3)(typescript@5.6.3)
|
||||
typescript:
|
||||
specifier: ^5.2.2
|
||||
version: 5.6.3
|
||||
|
||||
packages/integrations:
|
||||
dependencies:
|
||||
'@slack/bolt':
|
||||
@@ -7526,9 +7492,6 @@ packages:
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
csv-parse@5.6.0:
|
||||
resolution: {integrity: sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==}
|
||||
|
||||
d3-array@2.12.1:
|
||||
resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==}
|
||||
|
||||
@@ -9630,16 +9593,16 @@ packages:
|
||||
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
lucide-react@0.451.0:
|
||||
resolution: {integrity: sha512-OwQ3uljZLp2cerj8sboy5rnhtGTCl9UCJIhT1J85/yOuGVlEH+xaUPR7tvNdddPvmV5M5VLdr7cQuWE3hzA4jw==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
|
||||
|
||||
lucide-react@0.454.0:
|
||||
resolution: {integrity: sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
|
||||
|
||||
lucide-react@0.513.0:
|
||||
resolution: {integrity: sha512-CJZKq2g8Y8yN4Aq002GahSXbG2JpFv9kXwyiOAMvUBv7pxeOFHUWKB0mO7MiY4ZVFCV4aNjv2BJFq/z3DgKPQg==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
luxon@3.4.4:
|
||||
resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -19949,8 +19912,6 @@ snapshots:
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
csv-parse@5.6.0: {}
|
||||
|
||||
d3-array@2.12.1:
|
||||
dependencies:
|
||||
internmap: 1.0.1
|
||||
@@ -22559,14 +22520,14 @@ snapshots:
|
||||
dependencies:
|
||||
yallist: 4.0.0
|
||||
|
||||
lucide-react@0.451.0(react@18.2.0):
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
|
||||
lucide-react@0.454.0(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
lucide-react@0.513.0(react@18.2.0):
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
|
||||
luxon@3.4.4: {}
|
||||
|
||||
luxon@3.6.1: {}
|
||||
|
||||
Reference in New Issue
Block a user