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) {
-