From 4a54830cad2590ab281b61a8108571fbcb32b129 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jun 2025 13:11:36 +0200 Subject: [PATCH] Refactor filtering system with improved autocomplete and event handling - Add property type icons and improved filter categorization - Implement virtualized filter selection with VList - Update autocomplete to use new property-based endpoints - Add support for event vs property filter distinction - Improve top values fetching with proper caching - Add cards/try endpoint routing in API client - Various code style and formatting improvements Refactor filtering system with improved autocomplete and event handling --- frontend/app/api_client.ts | 31 +- .../components/WidgetForm/WidgetFormNew.tsx | 33 +- .../FilterAutoComplete/FilterAutoComplete.tsx | 93 +++-- .../shared/Filters/FilterItem/FilterItem.tsx | 199 ++++++---- .../Filters/FilterList/UnifiedFilterList.tsx | 136 ++++--- .../Filters/FilterModal/FilterModal.tsx | 359 +++++++++++------- .../FilterSelection/FilterSelection.tsx | 139 ++++--- .../Filters/FilterValue/FilterValue.tsx | 140 ++++--- .../Filters/FilterValue/ValueAutoComplete.tsx | 263 ++++++++----- frontend/app/mstore/filterStore.ts | 53 ++- frontend/app/mstore/types/filter.ts | 184 +++++---- frontend/app/mstore/types/filterConstants.ts | 205 +++++++--- frontend/app/mstore/types/filterItem.ts | 5 +- frontend/app/mstore/types/widget.ts | 8 +- frontend/app/services/FilterService.ts | 8 +- frontend/app/services/SearchService.ts | 14 +- frontend/app/types/filter/filterType.ts | 16 +- 17 files changed, 1201 insertions(+), 685 deletions(-) diff --git a/frontend/app/api_client.ts b/frontend/app/api_client.ts index 899cf904d..0fcbc3a9a 100644 --- a/frontend/app/api_client.ts +++ b/frontend/app/api_client.ts @@ -103,8 +103,8 @@ export default class APIClient { // Always fetch the latest JWT from the store const jwt = this.getJwt(); const headers = new Headers({ - 'Accept': 'application/json', - 'Content-Type': 'application/json' + Accept: 'application/json', + 'Content-Type': 'application/json', }); if (reqHeaders) { @@ -121,7 +121,7 @@ export default class APIClient { const init: RequestInit = { method, headers, - body: params ? JSON.stringify(params) : undefined + body: params ? JSON.stringify(params) : undefined, }; if (method === 'GET') { @@ -185,20 +185,28 @@ export default class APIClient { delete init.body; } - if (( - path.includes('login') - || path.includes('refresh') - || path.includes('logout') - || path.includes('reset') - ) && window.env.NODE_ENV !== 'development' + if ( + (path.includes('login') || + path.includes('refresh') || + path.includes('logout') || + path.includes('reset')) && + window.env.NODE_ENV !== 'development' ) { init.credentials = 'include'; } else { delete init.credentials; } - const noChalice = path.includes('/kai') || path.includes('v1/integrations') || path.includes('/spot') && !path.includes('/login'); + const noChalice = + path.includes('/kai') || + path.includes('v1/integrations') || + (path.includes('/spot') && !path.includes('/login')); let edp = window.env.API_EDP || window.location.origin + '/api'; + + if (path.includes('/cards/try')) { + // TODO - Remove this condition + edp = 'http://localhost:8080/v1/analytics'; + } if (noChalice && !edp.includes('api.openreplay.com')) { edp = edp.replace('/api', ''); } @@ -227,8 +235,7 @@ export default class APIClient { try { const errorData = await response.json(); errorMsg = errorData.errors?.[0] || errorMsg; - } catch { - } + } catch {} throw new Error(errorMsg); } diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx index a726fe0b2..2ef1b5772 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { Card, Space, Button, Alert, Form, Select, Tooltip } from 'antd'; -import { useStore } from 'App/mstore'; +import { projectStore, useStore } from 'App/mstore'; import { eventKeys } from 'Types/filter/newFilter'; import { HEATMAP, @@ -204,12 +204,17 @@ const FilterSection = observer( const PathAnalysisFilter = observer(({ metric, writeOption }: any) => { const { t } = useTranslation(); - const metricValueOptions = [ - { value: 'location', label: t('Pages') }, - { value: 'click', label: t('Clicks') }, - { value: 'input', label: t('Input') }, - { value: 'custom', label: t('Custom Events') }, - ]; + // const metricValueOptions = [ + // { value: 'location', label: t('Pages') }, + // { value: 'click', label: t('Clicks') }, + // { value: 'input', label: t('Input') }, + // { value: 'custom', label: t('Custom Events') }, + // ]; + // + const { filterStore } = useStore(); + const metricValueOptions = useMemo(() => { + return filterStore.getEventOptions(projectStore?.activeSiteId + ''); + }, []); const onPointChange = (value: any) => { writeOption({ name: 'startType', value: { value } }); @@ -261,12 +266,12 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => { metric.updateStartPoint(val)} onRemoveFilter={() => {}} /> diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx index fd5556752..d36a3ae21 100644 --- a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx @@ -1,9 +1,19 @@ -import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import React, { + useState, + useEffect, + useCallback, + useMemo, + useRef, +} from 'react'; import { debounce } from 'App/utils'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; import { searchService } from 'App/services'; -import { AutoCompleteContainer, AutocompleteModal, Props } from './AutocompleteModal'; +import { + AutoCompleteContainer, + AutocompleteModal, + Props, +} from './AutocompleteModal'; import { TopValue } from '@/mstore/filterStore'; interface FilterParams { @@ -37,15 +47,14 @@ function processMetadataValues(input: FilterParams): FilterParams { return result as FilterParams; // Cast back if confident, or adjust logic } - const FilterAutoComplete = observer( ({ - params, // Expect FilterParams type here - values, - onClose, - onApply, - placeholder - }: { + params, // Expect FilterParams type here + values, + onClose, + onApply, + placeholder, + }: { params: FilterParams; values: string[]; onClose: () => void; @@ -73,7 +82,10 @@ const FilterAutoComplete = observer( if (projectsStore.siteId && params.id) { setLoading(true); try { - await filterStore.fetchTopValues(params.id, projectsStore.siteId); + await filterStore.fetchTopValues({ + id: params.id, + siteId: projectsStore.siteId, + }); } catch (error) { console.error('Failed to load top values', error); // Handle error state if needed @@ -93,38 +105,43 @@ const FilterAutoComplete = observer( setOptions(mappedTopValues); }, [mappedTopValues]); - - const loadOptions = useCallback(async (inputValue: string) => { - if (!inputValue.length) { - setOptions(mappedTopValues); - return; - } - - setLoading(true); - try { - const searchType = params.name?.toLowerCase(); - if (!searchType) { - console.warn('Search type (params.name) is missing.'); - setOptions([]); + const loadOptions = useCallback( + async (inputValue: string) => { + if (!inputValue.length) { + setOptions(mappedTopValues); return; } - const data: { value: string }[] = await searchService.fetchAutoCompleteValues({ - type: searchType, - q: inputValue - }); - const _options = data.map((i) => ({ value: i.value, label: i.value })) || []; - setOptions(_options); - } catch (e) { - console.error('Failed to fetch autocomplete values:', e); - setOptions(mappedTopValues); - } finally { - setLoading(false); - } - }, [mappedTopValues, params.name, searchService.fetchAutoCompleteValues]); + setLoading(true); + try { + const searchType = params.name?.toLowerCase(); + if (!searchType) { + console.warn('Search type (params.name) is missing.'); + setOptions([]); + return; + } + const data: { value: string }[] = + await searchService.fetchAutoCompleteValues({ + type: searchType, + q: inputValue, + }); + const _options = + data.map((i) => ({ value: i.value, label: i.value })) || []; + setOptions(_options); + } catch (e) { + console.error('Failed to fetch autocomplete values:', e); + setOptions(mappedTopValues); + } finally { + setLoading(false); + } + }, + [mappedTopValues, params.name, searchService.fetchAutoCompleteValues], + ); - const debouncedLoadOptions = useCallback(debounce(loadOptions, 500), [loadOptions]); + const debouncedLoadOptions = useCallback(debounce(loadOptions, 500), [ + loadOptions, + ]); const handleInputChange = (newValue: string) => { debouncedLoadOptions(newValue); @@ -149,7 +166,7 @@ const FilterAutoComplete = observer( placeholder={placeholder} /> ); - } + }, ); function AutoCompleteController(props: Props) { diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx index 68b9723fc..d1975f570 100644 --- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx +++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx @@ -50,7 +50,7 @@ function FilterItem(props: Props) { onPropertyOrderChange, parentEventFilterOptions, isDragging, - isFirst = false // Default to false + isFirst = false, // Default to false } = props; const [eventFilterOptions, setEventFilterOptions] = useState([]); @@ -58,7 +58,9 @@ function FilterItem(props: Props) { const { filterStore } = useStore(); const allFilters = filterStore.getCurrentProjectFilters(); - const eventSelections = allFilters.filter((i) => i.isEvent === filter.isEvent); + const eventSelections = allFilters.filter( + (i) => i.isEvent === filter.isEvent, + ); const filterSelections = useMemo(() => { if (isSubItem) { @@ -67,7 +69,7 @@ function FilterItem(props: Props) { return eventSelections; }, [isSubItem, parentEventFilterOptions, eventSelections]); - const operatorOptions = getOperatorsByType(filter.type); + const operatorOptions = getOperatorsByType(filter.dataType); useEffect(() => { let isMounted = true; // Mounted flag @@ -81,10 +83,18 @@ function FilterItem(props: Props) { // Only set loading if not already loading for this specific fetch if (isMounted) setEventFiltersLoading(true); - const options = await filterStore.getEventFilters(fetchName); + const options = await filterStore.getEventFilters( + fetchName, + filter.autoCaptured, + ); // Check mount status AND if the relevant dependencies are still the same - if (isMounted && filter.name === fetchName && !isSubItem && filter.isEvent) { + if ( + isMounted && + filter.name === fetchName && + !isSubItem && + filter.isEvent + ) { // Avoid setting state if options haven't actually changed (optional optimization) // This requires comparing options, which might be complex/costly. // Sticking to setting state is usually fine if dependencies are stable. @@ -92,11 +102,21 @@ function FilterItem(props: Props) { } } catch (error) { console.error('Failed to load event filters:', error); - if (isMounted && filter.name === fetchName && !isSubItem && filter.isEvent) { + if ( + isMounted && + filter.name === fetchName && + !isSubItem && + filter.isEvent + ) { setEventFilterOptions([]); } } finally { - if (isMounted && filter.name === fetchName && !isSubItem && filter.isEvent) { + if ( + isMounted && + filter.name === fetchName && + !isSubItem && + filter.isEvent + ) { setEventFiltersLoading(false); } } @@ -124,6 +144,13 @@ function FilterItem(props: Props) { // that determine *if* and *what* to fetch. }, [filter.name, filter.isEvent, isSubItem, filterStore]); // + console.log('filter...', filter); + + const isUserEvent = useMemo( + () => filter.isEvent && !filter.autoCaptured, + [filter.isEvent, filter.autoCaptured], + ); + const canShowValues = useMemo( () => !( @@ -131,10 +158,13 @@ function FilterItem(props: Props) { filter.operator === 'onAny' || filter.operator === 'isUndefined' ), - [filter.operator] + [filter.operator], ); - const isReversed = useMemo(() => filter.key === FilterKey.TAGGED_ELEMENT, [filter.key]); + const isReversed = useMemo( + () => filter.key === FilterKey.TAGGED_ELEMENT, + [filter.key], + ); const replaceFilter = useCallback( (selectedFilter: any) => { @@ -144,56 +174,59 @@ function FilterItem(props: Props) { filters: selectedFilter.filters ? selectedFilter.filters.map((i: any) => ({ ...i, value: [''] })) : [], - operator: selectedFilter.operator // Ensure operator is carried over or reset if needed + operator: selectedFilter.operator, // Ensure operator is carried over or reset if needed }); }, - [onUpdate] + [onUpdate], ); const handleOperatorChange = useCallback( (e: any, { value }: any) => { onUpdate({ ...filter, operator: value }); }, - [filter, onUpdate] + [filter, onUpdate], ); const handleSourceOperatorChange = useCallback( (e: any, { value }: any) => { onUpdate({ ...filter, sourceOperator: value }); }, - [filter, onUpdate] + [filter, onUpdate], ); const handleUpdateSubFilter = useCallback( (subFilter: any, index: number) => { onUpdate({ ...filter, - filters: filter.filters.map((i: any, idx: number) => (idx === index ? subFilter : i)) + filters: filter.filters.map((i: any, idx: number) => + idx === index ? subFilter : i, + ), }); }, - [filter, onUpdate] + [filter, onUpdate], ); const handleRemoveSubFilter = useCallback( (index: number) => { onUpdate({ ...filter, - filters: filter.filters.filter((_: any, idx: number) => idx !== index) + filters: filter.filters.filter((_: any, idx: number) => idx !== index), }); }, - [filter, onUpdate] + [filter, onUpdate], ); const filteredSubFilters = useMemo( () => filter.filters ? filter.filters.filter( - (i: any) => - (i.key !== FilterKey.FETCH_REQUEST_BODY && i.key !== FilterKey.FETCH_RESPONSE_BODY) || - saveRequestPayloads - ) + (i: any) => + (i.key !== FilterKey.FETCH_REQUEST_BODY && + i.key !== FilterKey.FETCH_RESPONSE_BODY) || + saveRequestPayloads, + ) : [], - [filter.filters, saveRequestPayloads] + [filter.filters, saveRequestPayloads], ); const addSubFilter = useCallback( @@ -201,21 +234,25 @@ function FilterItem(props: Props) { const newSubFilter = { ...selectedFilter, value: selectedFilter.value || [''], - operator: selectedFilter.operator || 'is' + operator: selectedFilter.operator || 'is', }; onUpdate({ ...filter, - filters: [...(filter.filters || []), newSubFilter] + filters: [...(filter.filters || []), newSubFilter], }); }, - [filter, onUpdate] + [filter, onUpdate], ); const parentShowsIndex = !hideIndex; - const subFilterMarginLeftClass = parentShowsIndex ? 'ml-[1.75rem]' : 'ml-[0.75rem]'; + const subFilterMarginLeftClass = parentShowsIndex + ? 'ml-[1.75rem]' + : 'ml-[0.75rem]'; const subFilterPaddingLeftClass = parentShowsIndex ? 'pl-11' : 'pl-7'; - const categoryPart = filter?.subCategory ? filter.subCategory : filter?.category; + const categoryPart = filter?.subCategory + ? filter.subCategory + : filter?.category; const namePart = filter?.displayName || filter?.name; const hasCategory = Boolean(categoryPart); const hasName = Boolean(namePart); @@ -224,30 +261,34 @@ function FilterItem(props: Props) { return (
-
{/* Use items-start */} - {!isSubItem && !hideIndex && filterIndex !== undefined && filterIndex >= 0 && ( -
{/* Align index top */} - {filterIndex + 1} -
- )} - +
+ {' '} + {/* Use items-start */} + {!isSubItem && + !hideIndex && + filterIndex !== undefined && + filterIndex >= 0 && ( +
+ {' '} + {/* Align index top */} + {filterIndex + 1} +
+ )} {isSubItem && ( -
+
{subFilterIndex === 0 && ( - - where - + where )} {subFilterIndex !== 0 && propertyOrder && onPropertyOrderChange && ( - !readonly && onPropertyOrderChange(propertyOrder === 'and' ? 'or' : 'and') + !readonly && + onPropertyOrderChange(propertyOrder === 'and' ? 'or' : 'and') } > {propertyOrder} @@ -255,10 +296,8 @@ function FilterItem(props: Props) { )}
)} - {/* Main content area */} -
+
@@ -280,22 +319,21 @@ function FilterItem(props: Props) { {filter && ( {getIconForFilter(filter)} - + )} {/* Category/SubCategory */} {hasCategory && ( - {categoryPart} - + {categoryPart} + )} - {showSeparator && ( - - )} + {showSeparator && } - {hasName ? namePart : (hasCategory ? '' : defaultText)} {/* Show name or placeholder */} + {hasName ? namePart : hasCategory ? '' : defaultText}{' '} + {/* Show name or placeholder */} @@ -320,7 +358,7 @@ function FilterItem(props: Props) { )} - {operatorOptions.length > 0 && filter.type && ( + {operatorOptions.length > 0 && filter.dataType && !isUserEvent && ( <> {filter.value - .map((val: string) => - filter.options?.find((i: any) => i.value === val)?.label ?? val + .map( + (val: string) => + filter.options?.find((i: any) => i.value === val) + ?.label ?? val, ) .join(', ')}
) : ( -
{/* Wrap FilterValue */} - +
+ {' '} + {/* Wrap FilterValue */} +
))} @@ -368,11 +414,15 @@ function FilterItem(props: Props) { )} {/*
*/}
- {/* Action Buttons */} {!readonly && !hideDelete && ( -
{/* Align top */} - +
+ {' '} + {/* Align top */} + @@ -301,7 +372,7 @@ const ValueAutoComplete = observer( onOpenChange={handleOpenChange} placement="bottomLeft" arrow={false} - getPopupContainer={triggerNode => triggerNode || document.body} + getPopupContainer={(triggerNode) => triggerNode || document.body} > ); - } + }, ); export default ValueAutoComplete; diff --git a/frontend/app/mstore/filterStore.ts b/frontend/app/mstore/filterStore.ts index 2daf2a479..9d36db727 100644 --- a/frontend/app/mstore/filterStore.ts +++ b/frontend/app/mstore/filterStore.ts @@ -1,5 +1,5 @@ import { makeAutoObservable, runInAction } from 'mobx'; -import { filterService } from 'App/services'; +import { filterService, searchService } from 'App/services'; import { Filter, COMMON_FILTERS } from './types/filterConstants'; import { FilterKey } from 'Types/filter/filterType'; import { projectStore } from '@/mstore/index'; @@ -18,6 +18,14 @@ interface ProjectFilters { [projectId: string]: Filter[]; } +interface TopValuesParams { + id?: string; + siteId?: string; + source?: string; + isAutoCapture?: boolean; + isEvent?: boolean; +} + export default class FilterStore { topValues: TopValues = {}; filters: ProjectFilters = {}; @@ -33,6 +41,17 @@ export default class FilterStore { this.initCommonFilters(); } + getEventOptions = (sietId: string) => { + return this.getFilters(sietId) + .filter((i: Filter) => i.isEvent) + .map((i: Filter) => { + return { + label: i.displayName || i.name, + value: i.name, + }; + }); + }; + setTopValues = (key: string, values: Record | TopValue[]) => { const vals = Array.isArray(values) ? values : values.data; this.topValues[key] = vals?.filter( @@ -44,19 +63,24 @@ export default class FilterStore { this.topValues = {}; }; - fetchTopValues = async (id: string, siteId: string, source?: string) => { - const valKey = `${siteId}_${id}${source || ''}`; + fetchTopValues = async (params: TopValuesParams) => { + const valKey = `${params.siteId}_${params.id}${params.source || ''}`; if (this.topValues[valKey] && this.topValues[valKey].length) { return Promise.resolve(this.topValues[valKey]); } - const filter = this.filters[siteId]?.find((i) => i.id === id); + const filter = this.filters[params.siteId + '']?.find( + (i) => i.id === params.id, + ); if (!filter) { - console.error('Filter not found in store:', id); + console.error('Filter not found in store:', valKey); return Promise.resolve([]); } - return filterService - .fetchTopValues(filter.name?.toLowerCase(), source) + + return searchService + .fetchTopValues({ + [params.isEvent ? 'eventName' : 'propertyName']: filter.name, + }) .then((response: []) => { this.setTopValues(valKey, response); }); @@ -84,7 +108,7 @@ export default class FilterStore { ...filter, possibleTypes: filter.possibleTypes?.map((type) => type.toLowerCase()) || [], - type: filter.possibleTypes?.[0].toLowerCase() || 'string', + dataType: filter.dataType || 'string', category: category || 'custom', subCategory: category === 'events' @@ -157,7 +181,6 @@ export default class FilterStore { getEventFilters = async (eventName: string): Promise => { const cacheKey = `${projectStore.activeSiteId}_${eventName}`; - console.log('cacheKey store', cacheKey); if (this.filterCache[cacheKey]) { return this.filterCache[cacheKey]; } @@ -170,6 +193,7 @@ export default class FilterStore { this.pendingFetches[cacheKey] = this.fetchAndProcessPropertyFilters(eventName); const filters = await this.pendingFetches[cacheKey]; + console.log('filters', filters); runInAction(() => { this.filterCache[cacheKey] = filters; @@ -185,14 +209,19 @@ export default class FilterStore { private fetchAndProcessPropertyFilters = async ( eventName: string, + isAutoCapture?: boolean, ): Promise => { - const resp = await filterService.fetchProperties(eventName); + const resp = await filterService.fetchProperties(eventName, isAutoCapture); const names = resp.data.map((i: any) => i['name']); const activeSiteId = projectStore.activeSiteId + ''; return ( - this.filters[activeSiteId]?.filter((i: any) => names.includes(i.name)) || - [] + this.filters[activeSiteId] + ?.filter((i: any) => names.includes(i.name)) + .map((f: any) => ({ + ...f, + eventName, + })) || [] ); }; diff --git a/frontend/app/mstore/types/filter.ts b/frontend/app/mstore/types/filter.ts index d0b89d287..86a0e7daa 100644 --- a/frontend/app/mstore/types/filter.ts +++ b/frontend/app/mstore/types/filter.ts @@ -10,12 +10,12 @@ type FilterData = Partial & { operator?: string; sourceOperator?: string; source?: any; - filters?: FilterData[] + filters?: FilterData[]; }; export const checkFilterValue = (value: unknown): string[] => { if (Array.isArray(value)) { - return value.length === 0 ? [''] : value.map(val => String(val ?? '')); + return value.length === 0 ? [''] : value.map((val) => String(val ?? '')); } if (value === null || value === undefined) { return ['']; @@ -74,7 +74,7 @@ export interface IFilterStore { value: unknown, operator?: string, sourceOperator?: string, - source?: unknown + source?: unknown, ): void; } @@ -100,59 +100,69 @@ export default class FilterStore implements IFilterStore { constructor( initialFilters: FilterData[] = [], isConditional = false, - isMobile = false + isMobile = false, ) { this.isConditional = isConditional; this.isMobile = isMobile; - this.filters = initialFilters.map( - (filterData) => this.createFilterItemFromData(filterData) + this.filters = initialFilters.map((filterData) => + this.createFilterItemFromData(filterData), ); - makeAutoObservable(this, { - filters: observable.shallow, - excludes: observable.shallow, - eventsOrder: observable, - startTimestamp: observable, - endTimestamp: observable, - name: observable, - page: observable, - limit: observable, - autoOpen: observable, - filterId: observable, - eventsHeader: observable, - merge: action, - addFilter: action, - replaceFilters: action, - updateFilter: action, - removeFilter: action, - fromJson: action, - fromData: action, - addExcludeFilter: action, - updateExcludeFilter: action, - removeExcludeFilter: action, - addFunnelDefaultFilters: action, - addOrUpdateFilter: action, - addFilterByKeyAndValue: action, - isConditional: false, - isMobile: false, - eventsOrderSupport: false, - ID_KEY: false - }, { autoBind: true }); + makeAutoObservable( + this, + { + filters: observable.shallow, + excludes: observable.shallow, + eventsOrder: observable, + startTimestamp: observable, + endTimestamp: observable, + name: observable, + page: observable, + limit: observable, + autoOpen: observable, + filterId: observable, + eventsHeader: observable, + merge: action, + addFilter: action, + replaceFilters: action, + updateFilter: action, + removeFilter: action, + fromJson: action, + fromData: action, + addExcludeFilter: action, + updateExcludeFilter: action, + removeExcludeFilter: action, + addFunnelDefaultFilters: action, + addOrUpdateFilter: action, + addFilterByKeyAndValue: action, + isConditional: false, + isMobile: false, + eventsOrderSupport: false, + ID_KEY: false, + }, + { autoBind: true }, + ); } merge(filterData: Partial) { runInAction(() => { - const validKeys = Object.keys(this).filter(key => typeof (this as any)[key] !== 'function' && key !== 'eventsOrderSupport' && key !== 'isConditional' && key !== 'isMobile'); + const validKeys = Object.keys(this).filter( + (key) => + typeof (this as any)[key] !== 'function' && + key !== 'eventsOrderSupport' && + key !== 'isConditional' && + key !== 'isMobile', + ); for (const key in filterData) { if (validKeys.includes(key)) { (this as any)[key] = (filterData as any)[key]; } } if (filterData.filters) { - this.filters = filterData.filters.map(f => f); + this.filters = filterData.filters.map((f) => f); } if (filterData.excludes) { - this.excludes = filterData.excludes.map(f => f); + this.excludes = filterData.excludes.map((f) => f); } }); } @@ -165,10 +175,12 @@ export default class FilterStore implements IFilterStore { private createFilterItemFromData(filterData: FilterData): FilterItem { const dataWithValue = { ...filterData, - value: checkFilterValue(filterData.value) + value: checkFilterValue(filterData.value), }; if (Array.isArray(dataWithValue.filters)) { - dataWithValue.filters = dataWithValue.filters.map(nestedFilter => this.createFilterItemFromData(nestedFilter)); + dataWithValue.filters = dataWithValue.filters.map((nestedFilter) => + this.createFilterItemFromData(nestedFilter), + ); } return new FilterItem(dataWithValue); } @@ -179,7 +191,7 @@ export default class FilterStore implements IFilterStore { } replaceFilters(newFilters: FilterItem[]) { - this.filters = newFilters.map(f => f); + this.filters = newFilters.map((f) => f); } private updateFilterByIndex(index: number, filterData: FilterData) { @@ -194,22 +206,26 @@ export default class FilterStore implements IFilterStore { } updateFilter(filterId: string, filterData: FilterData) { - const index = this.filters.findIndex(f => f.id === filterId); + const index = this.filters.findIndex((f) => f.id === filterId); if (index > -1) { const updatedFilter = this.createFilterItemFromData(filterData); updatedFilter.id = filterId; // Ensure the ID remains the same this.filters[index] = updatedFilter; } else { - console.warn(`FilterStore.updateFilter: Filter with id ${filterId} not found.`); + console.warn( + `FilterStore.updateFilter: Filter with id ${filterId} not found.`, + ); } } removeFilter(filterId: string) { - const index = this.filters.findIndex(f => f.id === filterId); + const index = this.filters.findIndex((f) => f.id === filterId); if (index > -1) { this.filters.splice(index, 1); } else { - console.warn(`FilterStore.removeFilter: Filter with id ${filterId} not found.`); + console.warn( + `FilterStore.removeFilter: Filter with id ${filterId} not found.`, + ); } } @@ -218,20 +234,20 @@ export default class FilterStore implements IFilterStore { this.name = json.name ?? ''; this.filters = Array.isArray(json.filters) ? json.filters.map((filterJson: JsonData) => - new FilterItem().fromJson(filterJson) - ) + new FilterItem().fromJson(filterJson), + ) : []; this.excludes = Array.isArray(json.excludes) ? json.excludes.map((filterJson: JsonData) => - new FilterItem().fromJson(filterJson) - ) + new FilterItem().fromJson(filterJson), + ) : []; this.eventsOrder = json.eventsOrder ?? 'then'; this.startTimestamp = json.startTimestamp ?? 0; this.endTimestamp = json.endTimestamp ?? 0; this.page = json.page ?? 1; this.limit = json.limit ?? 10; - this.autoOpen = json.autoOpen ?? false; + // this.autoOpen = json.autoOpen ?? false; this.filterId = json.filterId ?? ''; this.eventsHeader = json.eventsHeader ?? 'EVENTS'; }); @@ -242,16 +258,16 @@ export default class FilterStore implements IFilterStore { runInAction(() => { this.name = data.name ?? ''; this.filters = Array.isArray(data.filters) - ? data.filters.map((filterData: JsonData) => - this.createFilterItemFromData(filterData) - // new FilterItem(undefined, this.isConditional, this.isMobile).fromData(filterData) - ) + ? data.filters.map( + (filterData: JsonData) => this.createFilterItemFromData(filterData), + // new FilterItem(undefined, this.isConditional, this.isMobile).fromData(filterData) + ) : []; this.excludes = Array.isArray(data.excludes) - ? data.excludes.map((filterData: JsonData) => - this.createFilterItemFromData(filterData) - // new FilterItem(undefined, this.isConditional, this.isMobile).fromData(filterData) - ) + ? data.excludes.map( + (filterData: JsonData) => this.createFilterItemFromData(filterData), + // new FilterItem(undefined, this.isConditional, this.isMobile).fromData(filterData) + ) : []; this.eventsOrder = data.eventsOrder ?? 'then'; this.startTimestamp = data.startTimestamp ?? 0; @@ -271,14 +287,16 @@ export default class FilterStore implements IFilterStore { filters: this.filters.map((filterItem) => filterItem.toJson()), eventsOrder: this.eventsOrder, startTimestamp: this.startTimestamp, - endTimestamp: this.endTimestamp + endTimestamp: this.endTimestamp, }; } createFilterByKey(key: FilterKey | string): FilterItem { const sourceMap = this.isConditional ? conditionalFiltersMap : filtersMap; const filterTemplate = sourceMap[key as FilterKey]; - const newFilterData = filterTemplate ? { ...filterTemplate, value: [''] } : { key: key, value: [''] }; + const newFilterData = filterTemplate + ? { ...filterTemplate, value: [''] } + : { key: key, value: [''] }; return this.createFilterItemFromData(newFilterData); // Use helper } @@ -294,7 +312,7 @@ export default class FilterStore implements IFilterStore { endTimestamp: this.endTimestamp, eventsHeader: this.eventsHeader, page: this.page, - limit: this.limit + limit: this.limit, }; } @@ -304,22 +322,26 @@ export default class FilterStore implements IFilterStore { } updateExcludeFilter(filterId: string, filterData: FilterData) { - const index = this.excludes.findIndex(f => f.id === filterId); + const index = this.excludes.findIndex((f) => f.id === filterId); if (index > -1) { const updatedExclude = this.createFilterItemFromData(filterData); updatedExclude.id = filterId; // Ensure the ID remains the same this.excludes[index] = updatedExclude; } else { - console.warn(`FilterStore.updateExcludeFilter: Exclude filter with id ${filterId} not found.`); + console.warn( + `FilterStore.updateExcludeFilter: Exclude filter with id ${filterId} not found.`, + ); } } removeExcludeFilter(filterId: string) { - const index = this.excludes.findIndex(f => f.id === filterId); + const index = this.excludes.findIndex((f) => f.id === filterId); if (index > -1) { this.excludes.splice(index, 1); } else { - console.warn(`FilterStore.removeExcludeFilter: Exclude filter with id ${filterId} not found.`); + console.warn( + `FilterStore.removeExcludeFilter: Exclude filter with id ${filterId} not found.`, + ); } } @@ -331,10 +353,12 @@ export default class FilterStore implements IFilterStore { this.addFilter({ ...locationFilterData, value: [''], - operator: 'isAny' + operator: 'isAny', }); } else { - console.warn(`FilterStore.addFunnelDefaultFilters: Default filter not found for key ${FilterKey.LOCATION}`); + console.warn( + `FilterStore.addFunnelDefaultFilters: Default filter not found for key ${FilterKey.LOCATION}`, + ); } const clickFilterData = filtersMap[FilterKey.CLICK]; @@ -342,10 +366,12 @@ export default class FilterStore implements IFilterStore { this.addFilter({ ...clickFilterData, value: [''], - operator: 'onAny' + operator: 'onAny', }); } else { - console.warn(`FilterStore.addFunnelDefaultFilters: Default filter not found for key ${FilterKey.CLICK}`); + console.warn( + `FilterStore.addFunnelDefaultFilters: Default filter not found for key ${FilterKey.CLICK}`, + ); } }); } @@ -354,7 +380,7 @@ export default class FilterStore implements IFilterStore { const index = this.filters.findIndex((f) => f.key === filterData.key); const dataWithCheckedValue = { ...filterData, - value: checkFilterValue(filterData.value) + value: checkFilterValue(filterData.value), }; if (index > -1) { @@ -369,7 +395,7 @@ export default class FilterStore implements IFilterStore { value: unknown, operator?: string, sourceOperator?: string, - source?: unknown + source?: unknown, ) { const sourceMap = this.isConditional ? conditionalFiltersMap : filtersMap; const defaultFilterData = sourceMap[key as FilterKey]; @@ -381,12 +407,20 @@ export default class FilterStore implements IFilterStore { value: checkFilterValue(value), operator: operator ?? defaultFilterData.operator, sourceOperator: sourceOperator ?? defaultFilterData.sourceOperator, - source: source ?? defaultFilterData.source + source: source ?? defaultFilterData.source, }; this.addOrUpdateFilter(newFilterData); } else { - console.warn(`FilterStore.addFilterByKeyAndValue: No default filter template found for key ${key}. Adding generic filter.`); - this.addOrUpdateFilter({ key: key, value: checkFilterValue(value), operator, sourceOperator, source }); + console.warn( + `FilterStore.addFilterByKeyAndValue: No default filter template found for key ${key}. Adding generic filter.`, + ); + this.addOrUpdateFilter({ + key: key, + value: checkFilterValue(value), + operator, + sourceOperator, + source, + }); } } } diff --git a/frontend/app/mstore/types/filterConstants.ts b/frontend/app/mstore/types/filterConstants.ts index cac56a66c..c1b460631 100644 --- a/frontend/app/mstore/types/filterConstants.ts +++ b/frontend/app/mstore/types/filterConstants.ts @@ -20,6 +20,7 @@ export interface Filter { displayName?: string; description?: string; possibleTypes?: string[]; + dataType?: string; autoCaptured?: boolean; metadataName?: string; category: string; // 'event' | 'filter' | 'action' | etc. @@ -38,125 +39,217 @@ export interface Filter { export const OPERATORS = { string: [ { value: 'is', label: 'is', displayName: 'Is', description: 'Exact match' }, - { value: 'isNot', label: 'is not', displayName: 'Is not', description: 'Not an exact match' }, - { value: 'contains', label: 'contains', displayName: 'Contains', description: 'Contains the string' }, + { + value: 'isNot', + label: 'is not', + displayName: 'Is not', + description: 'Not an exact match', + }, + { + value: 'contains', + label: 'contains', + displayName: 'Contains', + description: 'Contains the string', + }, { value: 'doesNotContain', label: 'does not contain', displayName: 'Does not contain', - description: 'Does not contain the string' + description: 'Does not contain the string', + }, + { + value: 'startsWith', + label: 'starts with', + displayName: 'Starts with', + description: 'Starts with the string', + }, + { + value: 'endsWith', + label: 'ends with', + displayName: 'Ends with', + description: 'Ends with the string', + }, + { + value: 'isBlank', + label: 'is blank', + displayName: 'Is blank', + description: 'Is empty or null', + }, + { + value: 'isNotBlank', + label: 'is not blank', + displayName: 'Is not blank', + description: 'Is not empty or null', }, - { value: 'startsWith', label: 'starts with', displayName: 'Starts with', description: 'Starts with the string' }, - { value: 'endsWith', label: 'ends with', displayName: 'Ends with', description: 'Ends with the string' }, - { value: 'isBlank', label: 'is blank', displayName: 'Is blank', description: 'Is empty or null' }, - { value: 'isNotBlank', label: 'is not blank', displayName: 'Is not blank', description: 'Is not empty or null' } ], number: [ - { value: 'equals', label: 'equals', displayName: 'Equals', description: 'Exactly equals the value' }, + { + value: 'equals', + label: 'equals', + displayName: 'Equals', + description: 'Exactly equals the value', + }, { value: 'doesNotEqual', label: 'does not equal', // Fixed: added space displayName: 'Does not equal', - description: 'Does not equal the value' + description: 'Does not equal the value', }, - { value: 'greaterThan', label: 'greater than', displayName: 'Greater than', description: 'Greater than the value' }, { - value: 'lessThan', label: 'less than', // Fixed: added space and lowercased - displayName: 'Less than', description: 'Less than the value' + value: 'greaterThan', + label: 'greater than', + displayName: 'Greater than', + description: 'Greater than the value', + }, + { + value: 'lessThan', + label: 'less than', // Fixed: added space and lowercased + displayName: 'Less than', + description: 'Less than the value', }, { value: 'greaterThanOrEquals', label: 'greater than or equals', // Fixed: added spaces and lowercased displayName: 'Greater than or equals', - description: 'Greater than or equal to the value' + description: 'Greater than or equal to the value', }, { value: 'lessThanOrEquals', label: 'less than or equals', // Fixed: added spaces and lowercased displayName: 'Less than or equals', - description: 'Less than or equal to the value' + description: 'Less than or equal to the value', }, { - value: 'isBlank', label: 'is blank', // Fixed: added space and lowercased - displayName: 'Is blank', description: 'Is empty or null' + value: 'isBlank', + label: 'is blank', // Fixed: added space and lowercased + displayName: 'Is blank', + description: 'Is empty or null', }, { - value: 'isNotBlank', label: 'is not blank', // Fixed: added spaces and lowercased - displayName: 'Is not blank', description: 'Is not empty or null' - } + value: 'isNotBlank', + label: 'is not blank', // Fixed: added spaces and lowercased + displayName: 'Is not blank', + description: 'Is not empty or null', + }, ], boolean: [ { - value: 'isTrue', label: 'is true', // Fixed: added space and lowercased - displayName: 'Is true', description: 'Value is true' + value: 'isTrue', + label: 'is true', // Fixed: added space and lowercased + displayName: 'Is true', + description: 'Value is true', }, { - value: 'isFalse', label: 'is false', // Fixed: added space and lowercased - displayName: 'Is false', description: 'Value is false' + value: 'isFalse', + label: 'is false', // Fixed: added space and lowercased + displayName: 'Is false', + description: 'Value is false', }, { - value: 'isBlank', label: 'is blank', // Fixed: added space and lowercased - displayName: 'Is blank', description: 'Is null' + value: 'isBlank', + label: 'is blank', // Fixed: added space and lowercased + displayName: 'Is blank', + description: 'Is null', }, { - value: 'isNotBlank', label: 'is not blank', // Fixed: added spaces and lowercased - displayName: 'Is not blank', description: 'Is not null' - } + value: 'isNotBlank', + label: 'is not blank', // Fixed: added spaces and lowercased + displayName: 'Is not blank', + description: 'Is not null', + }, ], date: [ - { value: 'on', label: 'on', displayName: 'On', description: 'On the exact date' }, { - value: 'notOn', label: 'not on', // Fixed: added space and lowercased - displayName: 'Not on', description: 'Not on the exact date' - }, - { value: 'before', label: 'before', displayName: 'Before', description: 'Before the date' }, - { value: 'after', label: 'after', displayName: 'After', description: 'After the date' }, - { - value: 'onOrBefore', label: 'on or before', // Fixed: added spaces and lowercased - displayName: 'On or before', description: 'On or before the date' + value: 'on', + label: 'on', + displayName: 'On', + description: 'On the exact date', }, { - value: 'onOrAfter', label: 'on or after', // Fixed: added spaces and lowercased - displayName: 'On or after', description: 'On or after the date' + value: 'notOn', + label: 'not on', // Fixed: added space and lowercased + displayName: 'Not on', + description: 'Not on the exact date', }, { - value: 'isBlank', label: 'is blank', // Fixed: added space and lowercased - displayName: 'Is blank', description: 'Is empty or null' + value: 'before', + label: 'before', + displayName: 'Before', + description: 'Before the date', }, { - value: 'isNotBlank', label: 'is not blank', // Fixed: added spaces and lowercased - displayName: 'Is not blank', description: 'Is not empty or null' - } + value: 'after', + label: 'after', + displayName: 'After', + description: 'After the date', + }, + { + value: 'onOrBefore', + label: 'on or before', // Fixed: added spaces and lowercased + displayName: 'On or before', + description: 'On or before the date', + }, + { + value: 'onOrAfter', + label: 'on or after', // Fixed: added spaces and lowercased + displayName: 'On or after', + description: 'On or after the date', + }, + { + value: 'isBlank', + label: 'is blank', // Fixed: added space and lowercased + displayName: 'Is blank', + description: 'Is empty or null', + }, + { + value: 'isNotBlank', + label: 'is not blank', // Fixed: added spaces and lowercased + displayName: 'Is not blank', + description: 'Is not empty or null', + }, ], array: [ - { value: 'contains', label: 'contains', displayName: 'Contains', description: 'Array contains the value' }, + { + value: 'contains', + label: 'contains', + displayName: 'Contains', + description: 'Array contains the value', + }, { value: 'doesNotContain', label: 'does not contain', // Fixed: added spaces and lowercased displayName: 'Does not contain', - description: 'Array does not contain the value' + description: 'Array does not contain the value', }, { - value: 'hasAny', label: 'has any', // Fixed: added space and lowercased - displayName: 'Has any', description: 'Array has any of the values' + value: 'hasAny', + label: 'has any', // Fixed: added space and lowercased + displayName: 'Has any', + description: 'Array has any of the values', }, { - value: 'hasAll', label: 'has all', // Fixed: added space and lowercased - displayName: 'Has all', description: 'Array has all of the values' + value: 'hasAll', + label: 'has all', // Fixed: added space and lowercased + displayName: 'Has all', + description: 'Array has all of the values', }, { - value: 'isEmpty', label: 'is empty', // Fixed: added space and lowercased - displayName: 'Is empty', description: 'Array is empty' + value: 'isEmpty', + label: 'is empty', // Fixed: added space and lowercased + displayName: 'Is empty', + description: 'Array is empty', }, { - value: 'isNotEmpty', label: 'is not empty', // Fixed: added spaces and lowercased - displayName: 'Is not empty', description: 'Array is not empty' - } - ] + value: 'isNotEmpty', + label: 'is not empty', // Fixed: added spaces and lowercased + displayName: 'Is not empty', + description: 'Array is not empty', + }, + ], }; export const COMMON_FILTERS: Filter[] = []; diff --git a/frontend/app/mstore/types/filterItem.ts b/frontend/app/mstore/types/filterItem.ts index a499e959f..b806353a0 100644 --- a/frontend/app/mstore/types/filterItem.ts +++ b/frontend/app/mstore/types/filterItem.ts @@ -23,13 +23,14 @@ export default class FilterItem { value?: string[]; propertyOrder?: string; filters?: FilterItem[]; + autoOpen?: boolean; constructor(data: any = {}) { makeAutoObservable(this); if (Array.isArray(data.filters)) { data.filters = data.filters.map( - (i: Record) => new FilterItem(i) + (i: Record) => new FilterItem(i), ); } data.operator = data.operator || 'is'; @@ -82,7 +83,7 @@ export default class FilterItem { source: this.name, filters: Array.isArray(this.filters) ? this.filters.map((i) => i.toJson()) - : [] + : [], }; const isMetadata = this.category === FilterCategory.METADATA; diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index 04975e425..959d4f9c8 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -395,12 +395,12 @@ export default class Widget { } else if (this.metricType === FUNNEL) { _data.funnel = new Funnel().fromJSON(data); } else if (this.metricType === TABLE) { - const count = data[0]['count']; - const vals = data[0]['values'].map((s: any) => + const count = data['count']; + const vals = data['values'].map((s: any) => new SessionsByRow().fromJson(s, count, this.metricOf), ); - _data['values'] = vals - _data['total'] = data[0]['total']; + _data['values'] = vals; + _data['total'] = data['total']; } else { if (data.hasOwnProperty('chart')) { _data['value'] = data.value; diff --git a/frontend/app/services/FilterService.ts b/frontend/app/services/FilterService.ts index cbd251561..d15b9b802 100644 --- a/frontend/app/services/FilterService.ts +++ b/frontend/app/services/FilterService.ts @@ -24,8 +24,12 @@ export default class FilterService { return await response.json(); }; - fetchProperties = async (name: string) => { - let path = `/pa/PROJECT_ID/properties/search?event_name=${name}`; + fetchProperties = async ( + eventName: string, + isAutoCapture: boolean = false, + ) => { + // en = eventName, ac = isAutoCapture + let path = `/pa/PROJECT_ID/properties/search?en=${eventName}&ac=${isAutoCapture}`; const response = await this.client.get(path); return await response.json(); }; diff --git a/frontend/app/services/SearchService.ts b/frontend/app/services/SearchService.ts index d6aa5abaf..e673f950b 100644 --- a/frontend/app/services/SearchService.ts +++ b/frontend/app/services/SearchService.ts @@ -46,8 +46,20 @@ export default class SearchService extends BaseService { return j.data; } + async fetchTopValues(params: {}): Promise { + const r = await this.client.get( + '/pa/PROJECT_ID/properties/autocomplete', + params, + ); + const j = await r.json(); + return j.data; + } + async fetchAutoCompleteValues(params: {}): Promise { - const r = await this.client.get('/PROJECT_ID/events/search', params); + const r = await this.client.get( + '/pa/PROJECT_ID/properties/autocomplete', + params, + ); const j = await r.json(); return j.data; } diff --git a/frontend/app/types/filter/filterType.ts b/frontend/app/types/filter/filterType.ts index 7c7e10f7b..ec03dd0da 100644 --- a/frontend/app/types/filter/filterType.ts +++ b/frontend/app/types/filter/filterType.ts @@ -210,9 +210,9 @@ export enum FilterKey { MISSING_RESOURCE = 'missingResource', SLOW_SESSION = 'slowSession', CLICK_RAGE = 'clickRage', - CLICK = 'click', - INPUT = 'input', - LOCATION = 'location', + CLICK = 'CLICK', + INPUT = 'INPUT', + LOCATION = 'LOCATION', VIEW = 'view', CONSOLE = 'console', METADATA = 'metadata', @@ -279,11 +279,7 @@ export enum FilterKey { AVG_REQUEST_LOADT_IME = 'avgRequestLoadTime', AVG_RESPONSE_TIME = 'avgResponseTime', AVG_SESSION_DURATION = 'avgSessionDuration', - AVG_TILL_FIRST_BYTE = 'avgTillFirstByte', - AVG_TIME_TO_INTERACTIVE = 'avgTimeToInteractive', - AVG_TIME_TO_RENDER = 'avgTimeToRender', - AVG_USED_JS_HEAP_SIZE = 'avgUsedJsHeapSize', - AVG_VISITED_PAGES = 'avgVisitedPages', + COUNT_REQUESTS = 'countRequests', COUNT_SESSIONS = 'countSessions', @@ -305,10 +301,6 @@ export enum FilterKey { PAGES_RESPONSE_TIME = 'pagesResponseTime', PAGES_RESPONSE_TIME_DISTRIBUTION = 'pagesResponseTimeDistribution', SESSIONS_PER_BROWSER = 'sessionsPerBrowser', - SLOWEST_DOMAINS = 'slowestDomains', - SPEED_LOCATION = 'speedLocation', - TIME_TO_RENDER = 'timeToRender', - IMPACTED_SESSIONS_BY_SLOW_PAGES = 'impactedSessionsBySlowPages', CLICKMAP_URL = 'clickMapUrl', FEATURE_FLAG = 'featureFlag',