fix(dashboard): breakdowns on profile properties
This commit is contained in:
@@ -10,18 +10,20 @@ import {
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useEventProperties } from '@/hooks/useEventProperties';
|
import { useEventProperties } from '@/hooks/useEventProperties';
|
||||||
import { useDispatch } from '@/redux';
|
|
||||||
import { shortId } from '@openpanel/common';
|
|
||||||
import type { IChartEvent } from '@openpanel/validation';
|
import type { IChartEvent } from '@openpanel/validation';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { FilterIcon } from 'lucide-react';
|
|
||||||
import { ArrowLeftIcon, DatabaseIcon, UserIcon } from 'lucide-react';
|
import { ArrowLeftIcon, DatabaseIcon, UserIcon } from 'lucide-react';
|
||||||
import VirtualList from 'rc-virtual-list';
|
import VirtualList from 'rc-virtual-list';
|
||||||
import { useEffect, useState } from 'react';
|
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
|
||||||
import { changeEvent } from '../../reportSlice';
|
|
||||||
|
|
||||||
interface FiltersComboboxProps {
|
interface PropertiesComboboxProps {
|
||||||
event: IChartEvent;
|
event?: IChartEvent;
|
||||||
|
children: (setOpen: Dispatch<SetStateAction<boolean>>) => React.ReactNode;
|
||||||
|
onSelect: (action: {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchHeader({
|
function SearchHeader({
|
||||||
@@ -44,24 +46,23 @@ function SearchHeader({
|
|||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onSearch(e.target.value)}
|
onChange={(e) => onSearch(e.target.value)}
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FiltersCombobox({ event }: FiltersComboboxProps) {
|
export function PropertiesCombobox({
|
||||||
const dispatch = useDispatch();
|
event,
|
||||||
|
children,
|
||||||
|
onSelect,
|
||||||
|
}: PropertiesComboboxProps) {
|
||||||
const { projectId } = useAppParams();
|
const { projectId } = useAppParams();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const properties = useEventProperties(
|
const properties = useEventProperties({
|
||||||
{
|
event: event?.name,
|
||||||
event: event.name,
|
projectId,
|
||||||
projectId,
|
});
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!event.name,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const [state, setState] = useState<'index' | 'event' | 'profile'>('index');
|
const [state, setState] = useState<'index' | 'event' | 'profile'>('index');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [direction, setDirection] = useState<'forward' | 'backward'>('forward');
|
const [direction, setDirection] = useState<'forward' | 'backward'>('forward');
|
||||||
@@ -99,27 +100,14 @@ export function FiltersCombobox({ event }: FiltersComboboxProps) {
|
|||||||
description: string;
|
description: string;
|
||||||
}) => {
|
}) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
dispatch(
|
onSelect(action);
|
||||||
changeEvent({
|
|
||||||
...event,
|
|
||||||
filters: [
|
|
||||||
...event.filters,
|
|
||||||
{
|
|
||||||
id: shortId(),
|
|
||||||
name: action.value,
|
|
||||||
operator: 'is',
|
|
||||||
value: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderIndex = () => {
|
const renderIndex = () => {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<SearchHeader onSearch={() => {}} value={search} />
|
{/* <SearchHeader onSearch={() => {}} value={search} /> */}
|
||||||
<DropdownMenuSeparator />
|
{/* <DropdownMenuSeparator /> */}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="group justify-between"
|
className="group justify-between"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -229,15 +217,7 @@ export function FiltersCombobox({ event }: FiltersComboboxProps) {
|
|||||||
setOpen(open);
|
setOpen(open);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>{children(setOpen)}</DropdownMenuTrigger>
|
||||||
<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"
|
|
||||||
onClick={() => setOpen((p) => !p)}
|
|
||||||
>
|
|
||||||
<FilterIcon size={12} /> Add filter
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="max-w-80" align="start">
|
<DropdownMenuContent className="max-w-80" align="start">
|
||||||
<AnimatePresence mode="wait" initial={false}>
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
{state === 'index' && (
|
{state === 'index' && (
|
||||||
@@ -5,11 +5,13 @@ import { Combobox } from '@/components/ui/combobox';
|
|||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useEventProperties } from '@/hooks/useEventProperties';
|
import { useEventProperties } from '@/hooks/useEventProperties';
|
||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
import { SplitIcon } from 'lucide-react';
|
import { ChevronsUpDownIcon, SplitIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { IChartBreakdown } from '@openpanel/validation';
|
import type { IChartBreakdown } from '@openpanel/validation';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { addBreakdown, changeBreakdown, removeBreakdown } from '../reportSlice';
|
import { addBreakdown, changeBreakdown, removeBreakdown } from '../reportSlice';
|
||||||
|
import { PropertiesCombobox } from './PropertiesCombobox';
|
||||||
import { ReportBreakdownMore } from './ReportBreakdownMore';
|
import { ReportBreakdownMore } from './ReportBreakdownMore';
|
||||||
import type { ReportEventMoreProps } from './ReportEventMore';
|
import type { ReportEventMoreProps } from './ReportEventMore';
|
||||||
|
|
||||||
@@ -46,42 +48,63 @@ export function ReportBreakdowns() {
|
|||||||
<div key={item.name} className="rounded-lg border bg-def-100">
|
<div key={item.name} className="rounded-lg border bg-def-100">
|
||||||
<div className="flex items-center gap-2 p-2 px-4">
|
<div className="flex items-center gap-2 p-2 px-4">
|
||||||
<ColorSquare>{index}</ColorSquare>
|
<ColorSquare>{index}</ColorSquare>
|
||||||
<Combobox
|
<PropertiesCombobox
|
||||||
icon={SplitIcon}
|
onSelect={(action) => {
|
||||||
className="flex-1"
|
|
||||||
searchable
|
|
||||||
value={item.name}
|
|
||||||
onChange={(value) => {
|
|
||||||
dispatch(
|
dispatch(
|
||||||
changeBreakdown({
|
changeBreakdown({
|
||||||
...item,
|
...item,
|
||||||
name: value,
|
name: action.value,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
items={properties}
|
>
|
||||||
placeholder="Select..."
|
{(setOpen) => (
|
||||||
/>
|
<Button
|
||||||
|
variant={'outline'}
|
||||||
|
onClick={() => setOpen((prev) => !prev)}
|
||||||
|
size={'sm'}
|
||||||
|
autoHeight
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<div className="row w-full gap-2 items-center">
|
||||||
|
<SplitIcon className="size-4" />
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</PropertiesCombobox>
|
||||||
<ReportBreakdownMore onClick={handleMore(item)} />
|
<ReportBreakdownMore onClick={handleMore(item)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<Combobox
|
<PropertiesCombobox
|
||||||
icon={SplitIcon}
|
onSelect={(action) => {
|
||||||
searchable
|
|
||||||
value={''}
|
|
||||||
onChange={(value) => {
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addBreakdown({
|
addBreakdown({
|
||||||
name: value,
|
name: action.value,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
items={properties}
|
>
|
||||||
placeholder="Select breakdown"
|
{(setOpen) => (
|
||||||
/>
|
<Button
|
||||||
|
variant={'outline'}
|
||||||
|
onClick={() => setOpen((prev) => !prev)}
|
||||||
|
size={'sm'}
|
||||||
|
autoHeight
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<div className="row w-full gap-2 items-center">
|
||||||
|
<SplitIcon className="size-4" />
|
||||||
|
Select breakdown
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</PropertiesCombobox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ import {
|
|||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import { shortId } from '@openpanel/common';
|
||||||
import { alphabetIds } from '@openpanel/constants';
|
import { alphabetIds } from '@openpanel/constants';
|
||||||
import type { IChartEvent } from '@openpanel/validation';
|
import type { IChartEvent } from '@openpanel/validation';
|
||||||
import { GanttChartIcon, HandIcon, Users } from 'lucide-react';
|
import { FilterIcon, GanttChartIcon, HandIcon, Users } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
addEvent,
|
addEvent,
|
||||||
changeEvent,
|
changeEvent,
|
||||||
@@ -35,9 +36,9 @@ import {
|
|||||||
reorderEvents,
|
reorderEvents,
|
||||||
} from '../reportSlice';
|
} from '../reportSlice';
|
||||||
import { EventPropertiesCombobox } from './EventPropertiesCombobox';
|
import { EventPropertiesCombobox } from './EventPropertiesCombobox';
|
||||||
|
import { PropertiesCombobox } from './PropertiesCombobox';
|
||||||
import type { ReportEventMoreProps } from './ReportEventMore';
|
import type { ReportEventMoreProps } from './ReportEventMore';
|
||||||
import { ReportEventMore } from './ReportEventMore';
|
import { ReportEventMore } from './ReportEventMore';
|
||||||
import { FiltersCombobox } from './filters/FiltersCombobox';
|
|
||||||
import { FiltersList } from './filters/FiltersList';
|
import { FiltersList } from './filters/FiltersList';
|
||||||
|
|
||||||
function SortableEvent({
|
function SortableEvent({
|
||||||
@@ -158,7 +159,37 @@ function SortableEvent({
|
|||||||
</button>
|
</button>
|
||||||
</DropdownMenuComposed>
|
</DropdownMenuComposed>
|
||||||
)}
|
)}
|
||||||
{showAddFilter && <FiltersCombobox event={event} />}
|
{showAddFilter && (
|
||||||
|
<PropertiesCombobox
|
||||||
|
event={event}
|
||||||
|
onSelect={(action) => {
|
||||||
|
dispatch(
|
||||||
|
changeEvent({
|
||||||
|
...event,
|
||||||
|
filters: [
|
||||||
|
...event.filters,
|
||||||
|
{
|
||||||
|
id: shortId(),
|
||||||
|
name: action.value,
|
||||||
|
operator: 'is',
|
||||||
|
value: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(setOpen) => (
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen((p) => !p)}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<FilterIcon size={12} /> Add filter
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</PropertiesCombobox>
|
||||||
|
)}
|
||||||
|
|
||||||
{showSegment &&
|
{showSegment &&
|
||||||
(event.segment === 'property_average' ||
|
(event.segment === 'property_average' ||
|
||||||
|
|||||||
@@ -88,8 +88,11 @@ export function getChartSql({
|
|||||||
const anyFilterOnProfile = event.filters.some((filter) =>
|
const anyFilterOnProfile = event.filters.some((filter) =>
|
||||||
filter.name.startsWith('profile.'),
|
filter.name.startsWith('profile.'),
|
||||||
);
|
);
|
||||||
|
const anyBreakdownOnProfile = breakdowns.some((breakdown) =>
|
||||||
|
breakdown.name.startsWith('profile.'),
|
||||||
|
);
|
||||||
|
|
||||||
if (anyFilterOnProfile) {
|
if (anyFilterOnProfile || anyBreakdownOnProfile) {
|
||||||
sb.joins.profiles = `LEFT ANY JOIN (SELECT * FROM ${TABLE_NAMES.profiles} FINAL WHERE project_id = ${escape(projectId)}) as profile on profile.id = profile_id`;
|
sb.joins.profiles = `LEFT ANY JOIN (SELECT * FROM ${TABLE_NAMES.profiles} FINAL WHERE project_id = ${escape(projectId)}) as profile on profile.id = profile_id`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +133,7 @@ export function getChartSql({
|
|||||||
sb.where.bar = `(${breakdowns.map((b) => getSelectPropertyKey(b.name)).join(',')}) IN (
|
sb.where.bar = `(${breakdowns.map((b) => getSelectPropertyKey(b.name)).join(',')}) IN (
|
||||||
SELECT ${breakdowns.map((b) => getSelectPropertyKey(b.name)).join(',')}
|
SELECT ${breakdowns.map((b) => getSelectPropertyKey(b.name)).join(',')}
|
||||||
FROM ${TABLE_NAMES.events}
|
FROM ${TABLE_NAMES.events}
|
||||||
|
${getJoins()}
|
||||||
${getWhere()}
|
${getWhere()}
|
||||||
GROUP BY ${breakdowns.map((b) => getSelectPropertyKey(b.name)).join(',')}
|
GROUP BY ${breakdowns.map((b) => getSelectPropertyKey(b.name)).join(',')}
|
||||||
ORDER BY count(*) DESC
|
ORDER BY count(*) DESC
|
||||||
|
|||||||
Reference in New Issue
Block a user