1 ? breakdownIndex : index,
+ visibleVariants.length > 1 ? visibleIndex : index,
),
}}
/>
diff --git a/apps/start/src/components/report-chart/funnel/index.tsx b/apps/start/src/components/report-chart/funnel/index.tsx
index 24bd6043..f13a0cb5 100644
--- a/apps/start/src/components/report-chart/funnel/index.tsx
+++ b/apps/start/src/components/report-chart/funnel/index.tsx
@@ -7,7 +7,9 @@ import { ReportChartEmpty } from '../common/empty';
import { ReportChartError } from '../common/error';
import { ReportChartLoading } from '../common/loading';
import { useReportChartContext } from '../context';
-import { Chart, Summary, Tables } from './chart';
+import { useVisibleFunnelBreakdowns } from '@/hooks/use-visible-funnel-breakdowns';
+import { Chart, Summary } from './chart';
+import { BreakdownList } from './breakdown-list';
export function ReportFunnelChart() {
const { isLazyLoading, report, shareId } = useReportChartContext();
@@ -24,6 +26,10 @@ export function ReportFunnelChart() {
),
);
+ // Hook for limiting which breakdowns are shown in the chart only
+ const { breakdowns: visibleBreakdowns, setVisibleSeries } =
+ useVisibleFunnelBreakdowns(res.data?.current ?? [], 10);
+
if (isLazyLoading || res.isLoading) {
return
;
}
@@ -36,19 +42,17 @@ export function ReportFunnelChart() {
return
;
}
+ const hasBreakdowns = res.data.current.length > 1;
+
return (
- {res.data.current.length > 1 &&
}
-
- {res.data.current.map((item, index) => (
-
- ))}
+ {hasBreakdowns &&
}
+
+
b.id)}
+ setVisibleSeries={setVisibleSeries}
+ />
);
}
diff --git a/apps/start/src/components/report/sidebar/EventPropertiesCombobox.tsx b/apps/start/src/components/report/sidebar/EventPropertiesCombobox.tsx
deleted file mode 100644
index 059c47c7..00000000
--- a/apps/start/src/components/report/sidebar/EventPropertiesCombobox.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Combobox } from '@/components/ui/combobox';
-import { useAppParams } from '@/hooks/use-app-params';
-import { useEventProperties } from '@/hooks/use-event-properties';
-import { useDispatch } from '@/redux';
-import { cn } from '@/utils/cn';
-import { DatabaseIcon } from 'lucide-react';
-
-import type { IChartEvent } from '@openpanel/validation';
-
-import { changeEvent } from '../reportSlice';
-
-interface EventPropertiesComboboxProps {
- event: IChartEvent;
-}
-
-export function EventPropertiesCombobox({
- event,
-}: EventPropertiesComboboxProps) {
- const dispatch = useDispatch();
- const { projectId } = useAppParams();
- const properties = useEventProperties(
- {
- event: event.name,
- projectId,
- },
- {
- enabled: !!event.name,
- },
- ).map((item) => ({
- label: item,
- value: item,
- }));
-
- return (
-
{
- dispatch(
- changeEvent({
- ...event,
- property: value,
- type: 'event',
- }),
- );
- }}
- >
-
-
- );
-}
diff --git a/apps/start/src/components/report/sidebar/ReportEvents.tsx b/apps/start/src/components/report/sidebar/ReportEvents.tsx
deleted file mode 100644
index 6cb03682..00000000
--- a/apps/start/src/components/report/sidebar/ReportEvents.tsx
+++ /dev/null
@@ -1,385 +0,0 @@
-import { ColorSquare } from '@/components/color-square';
-import { Button } from '@/components/ui/button';
-import { ComboboxEvents } from '@/components/ui/combobox-events';
-import { Input } from '@/components/ui/input';
-import { InputEnter } from '@/components/ui/input-enter';
-import { useAppParams } from '@/hooks/use-app-params';
-import { useDebounceFn } from '@/hooks/use-debounce-fn';
-import { useEventNames } from '@/hooks/use-event-names';
-import { useDispatch, useSelector } from '@/redux';
-import {
- DndContext,
- type DragEndEvent,
- KeyboardSensor,
- PointerSensor,
- closestCenter,
- useSensor,
- useSensors,
-} from '@dnd-kit/core';
-import {
- SortableContext,
- sortableKeyboardCoordinates,
- useSortable,
- verticalListSortingStrategy,
-} from '@dnd-kit/sortable';
-import { CSS } from '@dnd-kit/utilities';
-import { shortId } from '@openpanel/common';
-import { alphabetIds } from '@openpanel/constants';
-import type { IChartEventItem, IChartFormula } from '@openpanel/validation';
-import { FilterIcon, HandIcon, PlusIcon } from 'lucide-react';
-import { ReportSegment } from '../ReportSegment';
-import {
- addSerie,
- changeEvent,
- duplicateEvent,
- removeEvent,
- reorderEvents,
-} from '../reportSlice';
-import { EventPropertiesCombobox } from './EventPropertiesCombobox';
-import { PropertiesCombobox } from './PropertiesCombobox';
-import type { ReportEventMoreProps } from './ReportEventMore';
-import { ReportEventMore } from './ReportEventMore';
-import { FiltersList } from './filters/FiltersList';
-
-function SortableEvent({
- event,
- index,
- showSegment,
- showAddFilter,
- isSelectManyEvents,
- ...props
-}: {
- event: IChartEventItem;
- index: number;
- showSegment: boolean;
- showAddFilter: boolean;
- isSelectManyEvents: boolean;
-} & React.HTMLAttributes
) {
- const dispatch = useDispatch();
- const { attributes, listeners, setNodeRef, transform, transition } =
- useSortable({ id: event.id ?? '' });
-
- const style = {
- transform: CSS.Transform.toString(transform),
- transition,
- };
-
- const isEvent = event.type === 'event';
-
- return (
-
-
-
- {props.children}
-
-
- {/* Segment and Filter buttons - only for events */}
- {isEvent && (showSegment || showAddFilter) && (
-
- {showSegment && (
-
{
- dispatch(
- changeEvent({
- ...event,
- segment,
- }),
- );
- }}
- />
- )}
- {showAddFilter && (
- {
- dispatch(
- changeEvent({
- ...event,
- filters: [
- ...event.filters,
- {
- id: shortId(),
- name: action.value,
- operator: 'is',
- value: [],
- },
- ],
- }),
- );
- }}
- >
- {(setOpen) => (
-
- )}
-
- )}
-
- {showSegment && event.segment.startsWith('property_') && (
-
- )}
-
- )}
-
- {/* Filters - only for events */}
- {isEvent && !isSelectManyEvents &&
}
-
- );
-}
-
-export function ReportEvents() {
- const selectedEvents = useSelector((state) => state.report.series);
- const chartType = useSelector((state) => state.report.chartType);
- const dispatch = useDispatch();
- const { projectId } = useAppParams();
- const eventNames = useEventNames({
- projectId,
- });
-
- const showSegment = !['retention', 'funnel'].includes(chartType);
- const showAddFilter = !['retention'].includes(chartType);
- const showDisplayNameInput = !['retention'].includes(chartType);
- const isAddEventDisabled =
- (chartType === 'retention' || chartType === 'conversion') &&
- selectedEvents.length >= 2;
- const dispatchChangeEvent = useDebounceFn((event: IChartEventItem) => {
- dispatch(changeEvent(event));
- });
- const isSelectManyEvents = chartType === 'retention';
-
- const sensors = useSensors(
- useSensor(PointerSensor),
- useSensor(KeyboardSensor, {
- coordinateGetter: sortableKeyboardCoordinates,
- }),
- );
-
- const handleDragEnd = (event: DragEndEvent) => {
- const { active, over } = event;
-
- if (over && active.id !== over.id) {
- const oldIndex = selectedEvents.findIndex((e) => e.id === active.id);
- const newIndex = selectedEvents.findIndex((e) => e.id === over.id);
-
- dispatch(reorderEvents({ fromIndex: oldIndex, toIndex: newIndex }));
- }
- };
-
- const handleMore = (event: IChartEventItem) => {
- const callback: ReportEventMoreProps['onClick'] = (action) => {
- switch (action) {
- case 'remove': {
- return dispatch(
- removeEvent({
- id: event.id,
- }),
- );
- }
- case 'duplicate': {
- return dispatch(duplicateEvent(event));
- }
- }
- };
-
- return callback;
- };
-
- const dispatchChangeFormula = useDebounceFn((formula: IChartFormula) => {
- dispatch(changeEvent(formula));
- });
-
- const showFormula =
- chartType !== 'conversion' &&
- chartType !== 'funnel' &&
- chartType !== 'retention';
-
- return (
-
-
Metrics
-
- ({ id: e.id! }))}
- strategy={verticalListSortingStrategy}
- >
-
- {selectedEvents.map((event, index) => {
- const isFormula = event.type === 'formula';
-
- return (
-
- {isFormula ? (
- <>
-
- {
- dispatchChangeFormula({
- ...event,
- formula: value,
- });
- }}
- />
- {showDisplayNameInput && (
- {
- dispatchChangeFormula({
- ...event,
- displayName: e.target.value,
- });
- }}
- />
- )}
-
-
- >
- ) : (
- <>
- {
- dispatch(
- changeEvent(
- Array.isArray(value)
- ? {
- id: event.id,
- type: 'event',
- segment: 'user',
- filters: [
- {
- name: 'name',
- operator: 'is',
- value: value,
- },
- ],
- name: '*',
- }
- : {
- ...event,
- type: 'event',
- name: value,
- filters: [],
- },
- ),
- );
- }}
- items={eventNames}
- placeholder="Select event"
- />
- {showDisplayNameInput && (
- {
- dispatchChangeEvent({
- ...event,
- displayName: e.target.value,
- });
- }}
- />
- )}
-
- >
- )}
-
- );
- })}
-
-
- {
- if (isSelectManyEvents) {
- dispatch(
- addSerie({
- type: 'event',
- segment: 'user',
- name: value,
- filters: [
- {
- name: 'name',
- operator: 'is',
- value: [value],
- },
- ],
- }),
- );
- } else {
- dispatch(
- addSerie({
- type: 'event',
- name: value,
- segment: 'event',
- filters: [],
- }),
- );
- }
- }}
- placeholder="Select event"
- items={eventNames}
- />
- {showFormula && (
-
- )}
-
-
-
-
-
- );
-}
diff --git a/apps/start/src/components/report/sidebar/ReportSeriesItem.tsx b/apps/start/src/components/report/sidebar/ReportSeriesItem.tsx
index d647df98..e6f70aa2 100644
--- a/apps/start/src/components/report/sidebar/ReportSeriesItem.tsx
+++ b/apps/start/src/components/report/sidebar/ReportSeriesItem.tsx
@@ -3,10 +3,9 @@ import { useDispatch } from '@/redux';
import { shortId } from '@openpanel/common';
import { alphabetIds } from '@openpanel/constants';
import type { IChartEvent, IChartEventItem } from '@openpanel/validation';
-import { FilterIcon } from 'lucide-react';
+import { DatabaseIcon, FilterIcon, type LucideIcon } from 'lucide-react';
import { ReportSegment } from '../ReportSegment';
import { changeEvent } from '../reportSlice';
-import { EventPropertiesCombobox } from './EventPropertiesCombobox';
import { PropertiesCombobox } from './PropertiesCombobox';
import { FiltersList } from './filters/FiltersList';
@@ -90,19 +89,40 @@ export function ReportSeriesItem({
}}
>
{(setOpen) => (
-
+ Add filter
+
)}
)}
{showSegment && chartEvent.segment.startsWith('property_') && (
-
+ {
+ dispatch(
+ changeEvent({
+ ...chartEvent,
+ property: item.value,
+ type: 'event',
+ }),
+ );
+ }}
+ >
+ {(setOpen) => (
+ setOpen((p) => !p)}
+ >
+ {chartEvent.property
+ ? `Property: ${chartEvent.property}`
+ : 'Select property'}
+
+ )}
+
)}