Refactor filter handling and improve type safety

Remove unused excludeFilterKeys parameter, replace any types with
proper Filter interfaces, and update API response handling to use
events property instead of values.
This commit is contained in:
Shekar Siri 2025-06-02 17:59:32 +02:00
parent 2c390ab605
commit 7c40851dcb
14 changed files with 151 additions and 97 deletions

View file

@ -9,17 +9,16 @@ import { observer } from 'mobx-react-lite';
interface Props {
series: any;
excludeFilterKeys: Array<string>;
}
function AddStepButton({ series, excludeFilterKeys }: Props) {
function AddStepButton({ series }: Props) {
const { t } = useTranslation();
const { metricStore, filterStore } = useStore();
const metric: any = metricStore.instance;
const filters: Filter[] = filterStore.getCurrentProjectFilters();
// console.log('filters', filters)
const onAddFilter = (filter: any) => {
const onAddFilter = (filter: Filter) => {
console.log('Add Step Button', filter);
series.filter.addFilter(filter);
metric.updateKey('hasChanged', true);
};
@ -27,7 +26,7 @@ function AddStepButton({ series, excludeFilterKeys }: Props) {
<FilterSelection
filters={filters}
onFilterClick={onAddFilter}
mode={'filters'} // excludeFilterKeys={excludeFilterKeys}
// mode={'filters'} // excludeFilterKeys={excludeFilterKeys}
>
<Button
type="text"

View file

@ -40,12 +40,12 @@ function ExcludeFilters(props: Props) {
<FilterItem
hideIndex
filterIndex={index}
allowedFilterKeys={[
FilterKey.LOCATION,
FilterKey.CLICK,
FilterKey.INPUT,
FilterKey.CUSTOM,
]}
// allowedFilterKeys={[
// FilterKey.LOCATION,
// FilterKey.CLICK,
// FilterKey.INPUT,
// FilterKey.CUSTOM,
// ]}
filter={f}
onUpdate={(f) => onUpdateFilter(index, f)}
onRemoveFilter={() => onRemoveFilter(index)}

View file

@ -43,7 +43,7 @@ const FilterCountLabels = observer(
</Space>
</div>
);
}
},
);
const FilterSeriesHeader = observer(
@ -67,8 +67,8 @@ const FilterSeriesHeader = observer(
'px-4 ps-2 h-12 flex items-center relative bg-white border-gray-lighter border-t border-l border-r rounded-t-xl',
{
hidden: props.hidden,
'rounded-b-xl': !props.expanded
}
'rounded-b-xl': !props.expanded,
},
)}
>
<Space className="mr-auto" size={30}>
@ -111,7 +111,7 @@ const FilterSeriesHeader = observer(
</Space>
</div>
);
}
},
);
interface Props {
@ -135,20 +135,19 @@ interface Props {
function FilterSeries(props: Props) {
const {
observeChanges = () => {
},
observeChanges = () => {},
canDelete,
hideHeader = false,
emptyMessage = 'Add an event or filter step to define the series.',
supportsEmpty = true,
excludeFilterKeys = [],
// emptyMessage = 'Add an event or filter step to define the series.',
// supportsEmpty = true,
// excludeFilterKeys = [],
canExclude = false,
expandable = false,
isHeatmap,
removeEvents,
collapseState,
onToggleCollapse,
excludeCategory
// excludeCategory,
} = props;
const { filterStore } = useStore();
const expanded = isHeatmap || !collapseState;
@ -159,13 +158,18 @@ function FilterSeries(props: Props) {
const eventOptions: Filter[] = allFilterOptions.filter((i) => i.isEvent);
const propertyOptions: Filter[] = allFilterOptions.filter((i) => !i.isEvent);
const onUpdateFilter = (filterIndex: any, filter: any) => {
const onUpdateFilter = (filterIndex: number, filter: Filter) => {
series.filter.updateFilter(filterIndex, filter);
observeChanges();
};
const onFilterMove = (newFilters: any) => {
series.filter.replaceFilters(newFilters);
// const onFilterMove = (newFilters: Filter[]) => {
// series.filter.replaceFilters(newFilters);
// observeChanges();
// };
//
const onFilterMove = (draggedIndex: number, newPosition: number) => {
series.filter.moveFilter(draggedIndex, newPosition);
observeChanges();
};
@ -174,18 +178,18 @@ function FilterSeries(props: Props) {
observeChanges();
};
const onRemoveFilter = (filterIndex: any) => {
const onRemoveFilter = (filterIndex: number) => {
series.filter.removeFilter(filterIndex);
observeChanges();
};
const onAddFilter = (filter: any) => {
const onAddFilter = (filter: Filter) => {
filter.autoOpen = true;
series.filter.addFilter(filter);
observeChanges();
};
console.log('series.filter.filters', series.filter.filters);
console.log('series', series);
return (
<div>
@ -232,10 +236,12 @@ function FilterSeries(props: Props) {
<div className="bg-white rounded-b-xl border p-4">
<FilterListHeader
title={'Events'}
showEventsOrder={series.filter.filters.filter((f: any) => f.isEvent).length > 0}
showEventsOrder={
series.filter.filters.filter((f: any) => f.isEvent).length > 0
}
orderProps={{
eventsOrder: series.filter.eventsOrder,
eventsOrderSupport: ['then', 'and', 'or']
eventsOrderSupport: ['then', 'and', 'or'],
}}
onChangeOrder={onChangeEventsOrder}
filterSelection={
@ -271,7 +277,9 @@ function FilterSeries(props: Props) {
<FilterListHeader
title={'Filters'}
showEventsOrder={series.filter.filters.map((f: any) => !f.isEvent).length > 0}
showEventsOrder={
series.filter.filters.map((f: any) => !f.isEvent).length > 0
}
filterSelection={
<FilterSelection
filters={propertyOptions}
@ -290,7 +298,7 @@ function FilterSeries(props: Props) {
/>
<UnifiedFilterList
title="Events"
title="Filters"
filters={series.filter.filters.filter((f: any) => !f.isEvent)}
isDraggable={false}
showIndices={false}

View file

@ -10,10 +10,11 @@ import FilterSource from '../FilterSource';
import { useStore } from '@/mstore';
import { getIconForFilter } from 'Shared/Filters/FilterModal/FilterModal';
import { Filter, getOperatorsByType } from '@/mstore/types/filterConstants';
import { FilterItemData } from '@/mstore/types/filterItem';
interface Props {
filterIndex?: number;
filter: any;
filter: FilterItemData;
onUpdate: (filter: any) => void;
onRemoveFilter: () => void;
isFilter?: boolean;
@ -358,7 +359,7 @@ function FilterItem(props: Props) {
</>
)}
{operatorOptions.length > 0 && filter.dataType && !isUserEvent && (
{operatorOptions.length > 0 && filter.dataType && !filter.isEvent && (
<>
<FilterOperator
options={operatorOptions}

View file

@ -9,10 +9,10 @@ interface UnifiedFilterListProps {
filters: Filter[];
header?: React.ReactNode;
filterSelection?: React.ReactNode;
handleRemove: (key: string) => void;
handleUpdate: (key: string, updatedFilter: any) => void;
handleRemove: (index: string) => void;
handleUpdate: (index: string, updatedFilter: Filter) => void;
handleAdd: (newFilter: Filter) => void;
handleMove: (draggedIndex: number, newPosition: number) => void;
handleMove?: (draggedIndex: number, newPosition: number) => void;
isDraggable?: boolean;
showIndices?: boolean;
readonly?: boolean;
@ -141,7 +141,7 @@ const UnifiedFilterList = (props: UnifiedFilterListProps) => {
!(dragInd === hoverIndex && hoverPosition === 'top') &&
!(dragInd === hoverIndex - 1 && hoverPosition === 'bottom')
) {
handleMove(dragInd, newPosition);
handleMove?.(dragInd, newPosition);
}
setHoveredItem({ i: null, position: null });

View file

@ -96,7 +96,7 @@ const ValueAutoComplete = observer(
}, [initialValues]);
useEffect(() => {
if (filterKey && !filterStore.topValues[filterKey]) {
if (!params.isEvent && filterKey && !filterStore.topValues[filterKey]) {
setLoadingTopValues(true);
filterStore
.fetchTopValues({
@ -155,10 +155,10 @@ const ValueAutoComplete = observer(
autoCompleteParams.eventName = params.eventName;
}
const data: { values: any[] }[] =
const data: { events: any[] }[] =
await searchService.fetchAutoCompleteValues(autoCompleteParams);
const _options =
data.values?.map((i: any) => ({
data.events?.map((i: any) => ({
value: i.value,
label: i.value,
})) || [];

View file

@ -8,7 +8,6 @@ import { Plus } from 'lucide-react';
import { Filter } from '@/mstore/types/filterConstants';
import FilterListHeader from 'Shared/Filters/FilterList/FilterListHeader';
function SessionFilters() {
const { searchStore, filterStore } = useStore();
const searchInstance = searchStore.instance;
@ -23,13 +22,20 @@ function SessionFilters() {
const onChangeEventsOrder = (
_e: React.MouseEvent<HTMLButtonElement>,
{ value }: { value: string }
{ value }: { value: string },
) => {
searchStore.edit({
eventsOrder: value
eventsOrder: value,
});
};
const moveFilter = (index: number, newIndex: number) => {
const updatedFilters = [...searchInstance.filters];
const filterToMove = updatedFilters.splice(index, 1)[0];
updatedFilters.splice(newIndex, 0, filterToMove);
searchStore.edit({ filters: updatedFilters });
};
const eventFilters = searchInstance.filters.filter((i) => i.isEvent);
const attributeFilters = searchInstance.filters.filter((i) => !i.isEvent);
@ -66,7 +72,7 @@ function SessionFilters() {
handleRemove={searchStore.removeFilter}
handleUpdate={searchStore.updateFilter}
handleAdd={searchStore.addFilter}
handleMove={searchStore.moveFilter}
handleMove={moveFilter}
/>
<Divider className="my-3" />
@ -99,7 +105,7 @@ function SessionFilters() {
handleRemove={searchStore.removeFilter}
handleUpdate={searchStore.updateFilter}
handleAdd={searchStore.addFilter}
handleMove={searchStore.moveFilter}
// handleMove={moveFilter}
/>
</Card>
);

View file

@ -95,8 +95,10 @@ export default class FilterStore {
[params.isEvent ? 'eventName' : 'propertyName']: filter.name,
});
console.log('response', response.events);
runInAction(() => {
this.setTopValues(valKey, response);
this.setTopValues(valKey, response.events);
});
return this.topValues[valKey] || [];
@ -221,7 +223,10 @@ export default class FilterStore {
return this.getAllFilters(String(projectStore.activeSiteId));
};
getEventFilters = async (eventName: string): Promise<Filter[]> => {
getEventFilters = async (
eventName: string,
isAutoCapture: boolean,
): Promise<Filter[]> => {
const cacheKey = `${projectStore.activeSiteId}_${eventName}`;
// Check cache with TTL
@ -236,8 +241,10 @@ export default class FilterStore {
}
try {
this.pendingFetches[cacheKey] =
this.fetchAndProcessPropertyFilters(eventName);
this.pendingFetches[cacheKey] = this.fetchAndProcessPropertyFilters(
eventName,
isAutoCapture,
);
const filters = await this.pendingFetches[cacheKey];
runInAction(() => {

View file

@ -405,7 +405,7 @@ class SearchStore {
this.instance = Object.assign(this.instance, search);
};
updateFilter = (id: string, search: Partial<FilterItem>) => {
updateFilter = (id: string, search: Partial<Filter>) => {
const newFilters = this.instance.filters.map((f: any) => {
if (f.id === id) {
return {

View file

@ -11,6 +11,7 @@ type FilterData = Partial<FilterItem> & {
sourceOperator?: string;
source?: any;
filters?: FilterData[];
isEvent?: boolean;
};
export const checkFilterValue = (value: unknown): string[] => {
@ -172,6 +173,18 @@ export default class FilterStore implements IFilterStore {
this[key] = value;
}
moveFilter(index: number, newIndex: number) {
if (
index >= 0 &&
index < this.filters.length &&
newIndex >= 0 &&
newIndex < this.filters.length
) {
const [removed] = this.filters.splice(index, 1);
this.filters.splice(newIndex, 0, removed);
}
}
private createFilterItemFromData(filterData: FilterData): FilterItem {
const dataWithValue = {
...filterData,

View file

@ -34,6 +34,7 @@ export interface Filter {
value?: string[];
propertyOrder?: string;
filters?: Filter[];
autoOpen?: boolean;
}
export const OPERATORS = {

View file

@ -4,13 +4,13 @@ import { FilterProperty, Operator } from '@/mstore/types/filterConstants';
type JsonData = Record<string, any>;
// Define a proper interface for initialization data
interface FilterItemData {
export interface IFilter {
id?: string;
name?: string;
displayName?: string;
description?: string;
possibleTypes?: string[];
dataType?: string;
autoCaptured?: boolean;
metadataName?: string;
category?: string;
@ -23,57 +23,55 @@ interface FilterItemData {
isEvent?: boolean;
value?: string[];
propertyOrder?: string;
filters?: FilterItemData[];
filters?: IFilter[];
autoOpen?: boolean;
}
// Define valid keys that can be updated
type FilterItemKeys = keyof FilterItemData;
type FilterItemKeys = keyof IFilter;
export default class FilterItem {
id: string = '';
name: string = '';
displayName?: string;
description?: string;
possibleTypes?: string[];
autoCaptured?: boolean;
metadataName?: string;
displayName?: string = '';
description?: string = '';
possibleTypes?: string[] = [];
dataType?: string = '';
autoCaptured?: boolean = false;
// metadataName?: string = '';
category: string = '';
subCategory?: string;
type?: string;
icon?: string;
properties?: FilterProperty[];
operator?: string;
operators?: Operator[];
isEvent?: boolean;
value?: string[];
propertyOrder?: string;
filters?: FilterItem[];
autoOpen?: boolean;
subCategory?: string = '';
type?: string = '';
icon?: string = '';
properties?: FilterProperty[] = [];
operator?: string = '';
operators?: Operator[] = [];
isEvent?: boolean = false;
value?: string[] = [''];
propertyOrder?: string = '';
filters?: FilterItem[] = [];
autoOpen?: boolean = false;
constructor(data: FilterItemData = {}) {
constructor(data: IFilter = {}) {
makeAutoObservable(this);
this.initializeFromData(data);
}
private initializeFromData(data: FilterItemData): void {
// Set default operator if not provided
private initializeFromData(data: IFilter): void {
const processedData = {
...data,
operator: data.operator || 'is',
};
// Handle filters array transformation
if (Array.isArray(data.filters)) {
processedData.filters = data.filters.map(
(filterData: FilterItemData) => new FilterItem(filterData),
(filterData: IFilter) => new FilterItem(filterData),
);
}
this.merge(processedData);
}
updateKey<K extends FilterItemKeys>(key: K, value: FilterItemData[K]): void {
updateKey<K extends FilterItemKeys>(key: K, value: IFilter[K]): void {
if (key in this) {
(this as any)[key] = value;
} else {
@ -81,7 +79,8 @@ export default class FilterItem {
}
}
merge(data: FilterItemData): void {
merge(data: IFilter): void {
console.log('Object.entries(data)', Object.entries(data));
Object.entries(data).forEach(([key, value]) => {
if (key in this && value !== undefined) {
(this as any)[key] = value;
@ -89,7 +88,7 @@ export default class FilterItem {
});
}
fromData(data: FilterItemData): FilterItem {
fromData(data: IFilter): FilterItem {
if (!data) {
console.warn('fromData called with null/undefined data');
return this;
@ -98,14 +97,15 @@ export default class FilterItem {
Object.assign(this, data);
this.type = 'string';
this.name = data.name || '';
this.dataType = data.dataType || '';
this.category = data.category || '';
this.subCategory = data.subCategory;
this.operator = data.operator;
this.isEvent = Boolean(data.isEvent);
// Safely handle filters array
if (Array.isArray(data.filters)) {
this.filters = data.filters.map(
(filterData: FilterItemData) => new FilterItem(filterData),
(filterData: IFilter) => new FilterItem(filterData),
);
} else {
this.filters = [];
@ -121,13 +121,16 @@ export default class FilterItem {
}
this.type = 'string';
this.name = data.type || '';
this.category = data.category || '';
this.subCategory = data.subCategory;
this.operator = data.operator;
this.value = Array.isArray(data.value) ? data.value : [''];
// Safely handle filters array
this.name = data.name || '';
this.isEvent = Boolean(data.isEvent);
this.autoCaptured = Boolean(data.autoCaptured);
this.dataType = data.dataType || '';
if (Array.isArray(data.filters)) {
this.filters = data.filters.map(
(filterData: JsonData) => new FilterItem(filterData),
@ -142,7 +145,6 @@ export default class FilterItem {
toJson(): JsonData {
const json: JsonData = {
type: this.name,
isEvent: Boolean(this.isEvent),
value:
this.value?.map((item: any) => (item ? item.toString() : '')) || [],
operator: this.operator,
@ -150,9 +152,14 @@ export default class FilterItem {
filters: Array.isArray(this.filters)
? this.filters.map((filter) => filter.toJson())
: [],
// these props are required to get the source filter later
isEvent: Boolean(this.isEvent),
name: this.name,
autoCaptured: this.autoCaptured,
dataType: this.dataType,
};
// Handle metadata category
const isMetadata = this.category === FilterCategory.METADATA;
if (isMetadata) {
json.type = FilterKey.METADATA;
@ -160,7 +167,6 @@ export default class FilterItem {
json.sourceOperator = this.operator;
}
// Handle duration type
if (this.type === FilterKey.DURATION) {
json.value = this.value?.map((item: any) => (item ? Number(item) : 0));
}
@ -168,7 +174,6 @@ export default class FilterItem {
return json;
}
// Additional utility methods
isValid(): boolean {
return Boolean(this.name && this.category);
}

View file

@ -2,7 +2,21 @@ import { makeAutoObservable, observable, action } from 'mobx';
import FilterStore from './filter';
import { JsonData } from '@/mstore/types/filterConstants';
export default class FilterSeries {
export interface IFilterSeries {
seriesId?: any;
name: string;
filter: FilterStore;
update(key: any, value: any): void;
fromJson(json: JsonData, isHeatmap?: boolean): this;
fromData(data: any): this;
toJson(): {
seriesId?: any;
name: string;
filter: ReturnType<FilterStore['toJson']>;
};
}
export default class FilterSeries implements IFilterSeries {
public static get ID_KEY(): string {
return 'seriesId';
}
@ -16,7 +30,7 @@ export default class FilterSeries {
name: observable,
filter: observable.shallow,
update: action
update: action,
});
}
@ -30,7 +44,7 @@ export default class FilterSeries {
this.name = json.name;
this.filter = new FilterStore().fromJson(
json.filter || { filters: [] },
isHeatmap
isHeatmap,
);
return this;
}
@ -46,7 +60,7 @@ export default class FilterSeries {
return {
seriesId: this.seriesId,
name: this.name,
filter: this.filter.toJson()
filter: this.filter.toJson(),
};
}
}

View file

@ -1,13 +1,13 @@
import {
CUSTOM_RANGE,
DATE_RANGE_VALUES,
getDateRangeFromValue
getDateRangeFromValue,
} from 'App/dateRange';
import FilterItem from 'App/mstore/types/filterItem';
import { makeAutoObservable, observable } from 'mobx';
import { LAST_24_HOURS, LAST_30_DAYS, LAST_7_DAYS } from 'Types/app/period';
import { roundToNextMinutes } from '@/utils';
// import { Filter } from '@/mstore/types/filterConstants';
import { Filter } from '@/mstore/types/filterConstants';
// @ts-ignore
const rangeValue = DATE_RANGE_VALUES.LAST_24_HOURS;
@ -75,7 +75,7 @@ export default class Search {
constructor(initialData?: Partial<ISearch>) {
makeAutoObservable(this, {
filters: observable
filters: observable,
});
Object.assign(this, {
name: '',
@ -105,7 +105,7 @@ export default class Search {
strict: false,
eventsOrder: 'then',
limit: 10,
...initialData
...initialData,
});
}
@ -145,7 +145,7 @@ export default class Search {
toSearch() {
const js: any = { ...this };
js.filters = this.filters.map((filter: any) =>
new FilterItem(filter).toJson()
new FilterItem(filter).toJson(),
);
const { startDate, endDate } = this.getDateRange(
@ -182,7 +182,7 @@ export default class Search {
case CUSTOM_RANGE:
if (!customStartDate || !customEndDate) {
throw new Error(
'Start date and end date must be provided for CUSTOM_RANGE.'
'Start date and end date must be provided for CUSTOM_RANGE.',
);
}
startDate = customStartDate;