diff --git a/apps/start/src/components/report/sidebar/filters/FilterItem.tsx b/apps/start/src/components/report/sidebar/filters/FilterItem.tsx index e9a3dc57..ef7e0926 100644 --- a/apps/start/src/components/report/sidebar/filters/FilterItem.tsx +++ b/apps/start/src/components/report/sidebar/filters/FilterItem.tsx @@ -35,6 +35,7 @@ interface PureFilterProps { filter: IChartEventFilter, ) => void; className?: string; + immediateInput?: boolean; } export function FilterItem({ filter, event }: FilterProps) { @@ -113,6 +114,7 @@ export function PureFilterItem({ onChangeValue, onChangeOperator, className, + immediateInput, }: PureFilterProps) { const { projectId } = useAppParams(); @@ -170,6 +172,7 @@ export function PureFilterItem({ changeFilterValue([value])} + immediate={immediateInput} /> )} diff --git a/apps/start/src/components/settings/edit-project-filters.tsx b/apps/start/src/components/settings/edit-project-filters.tsx index 4e38f592..90fa0df1 100644 --- a/apps/start/src/components/settings/edit-project-filters.tsx +++ b/apps/start/src/components/settings/edit-project-filters.tsx @@ -1,16 +1,26 @@ import { WithLabel } from '@/components/forms/input-with-label'; import TagInput from '@/components/forms/tag-input'; +import { PureFilterItem } from '@/components/report/sidebar/filters/FilterItem'; +import { PropertiesCombobox } from '@/components/report/sidebar/PropertiesCombobox'; import { Button } from '@/components/ui/button'; +import { ComboboxEvents } from '@/components/ui/combobox-events'; import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; +import { useEventNames } from '@/hooks/use-event-names'; import { handleError, useTRPC } from '@/integrations/trpc/react'; import type { RouterOutputs } from '@/trpc/client'; import { zodResolver } from '@hookform/resolvers/zod'; +import { shortId } from '@openpanel/common'; import type { + IChartEventFilter, + IChartEventFilterOperator, + IChartEventFilterValue, + IProjectFilterEvent, IProjectFilterIp, IProjectFilterProfileId, } from '@openpanel/validation'; +import { zProjectFilterEvent } from '@openpanel/validation'; import { useMutation } from '@tanstack/react-query'; -import { SaveIcon } from 'lucide-react'; +import { PlusIcon, SaveIcon, Trash2Icon } from 'lucide-react'; import { Controller, useForm } from 'react-hook-form'; import { toast } from 'sonner'; import { z } from 'zod'; @@ -22,9 +32,125 @@ type Props = { const validator = z.object({ ips: z.array(z.string()), profileIds: z.array(z.string()), + eventRules: z.array(zProjectFilterEvent.omit({ type: true })), }); type IForm = z.infer; +type IEventRule = IForm['eventRules'][number]; + +interface EventRuleItemProps { + projectId: string; + rule: IEventRule; + onChange: (rule: IEventRule) => void; + onRemove: () => void; +} + +function EventRuleItem({ + projectId, + rule, + onChange, + onRemove, +}: EventRuleItemProps) { + const eventNames = useEventNames({ projectId, anyEvents: false }); + + const addFilter = (action: { + value: string; + label: string; + description: string; + }) => { + onChange({ + ...rule, + filters: [ + ...rule.filters, + { id: shortId(), name: action.value, operator: 'is', value: [] }, + ], + }); + }; + + const removeFilter = (filter: IChartEventFilter) => { + onChange({ + ...rule, + filters: rule.filters.filter((f) => f.id !== filter.id), + }); + }; + + const changeFilterValue = ( + value: IChartEventFilterValue[], + filter: IChartEventFilter, + ) => { + onChange({ + ...rule, + filters: rule.filters.map((f) => + f.id === filter.id ? { ...f, value } : f, + ), + }); + }; + + const changeFilterOperator = ( + operator: IChartEventFilterOperator, + filter: IChartEventFilter, + ) => { + onChange({ + ...rule, + filters: rule.filters.map((f) => + f.id === filter.id + ? { ...f, operator, value: f.value.filter(Boolean).slice(0, 1) } + : f, + ), + }); + }; + + return ( +
+
+
+ onChange({ ...rule, name })} + className="w-full" + searchable + /> +
+ +
+ + {rule.filters.length > 0 && ( + <> + {rule.filters.map((filter) => ( + + ))} + + )} +
+ + {(setOpen) => ( + + )} + +
+
+ ); +} export default function EditProjectFilters({ project }: Props) { const form = useForm({ @@ -38,6 +164,15 @@ export default function EditProjectFilters({ project }: Props) { (item): item is IProjectFilterProfileId => item.type === 'profile_id', ) .map((item) => item.profileId), + eventRules: project.filters + .filter((item): item is IProjectFilterEvent => item.type === 'event') + .map(({ name, filters, segment, property, displayName }) => ({ + name, + filters, + segment, + property, + displayName, + })), }, }); @@ -60,10 +195,38 @@ export default function EditProjectFilters({ project }: Props) { type: 'profile_id' as const, profileId, })), + ...values.eventRules + .filter((rule) => rule.name) + .map((rule) => ({ + type: 'event' as const, + ...rule, + })), ], }); }; + const eventRules = form.watch('eventRules'); + + const addEventRule = () => { + form.setValue('eventRules', [ + ...eventRules, + { name: '', filters: [], segment: 'event' }, + ]); + }; + + const updateEventRule = (index: number, rule: IEventRule) => { + const updated = [...eventRules]; + updated[index] = rule; + form.setValue('eventRules', updated); + }; + + const removeEventRule = (index: number) => { + form.setValue( + 'eventRules', + eventRules.filter((_, i) => i !== index), + ); + }; + return ( @@ -73,7 +236,15 @@ export default function EditProjectFilters({ project }: Props) {

-
+ { + if (e.key === 'Enter' && e.target instanceof HTMLInputElement) { + e.preventDefault(); + } + }} + className="space-y-4" + > + +
+ {eventRules.map((rule, index) => ( + updateEventRule(index, updated)} + onRemove={() => removeEventRule(index)} + /> + ))} + +
+
+