diff --git a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx index 99e830466..a002346d5 100644 --- a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx +++ b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx @@ -11,39 +11,41 @@ import { useStore } from 'App/mstore'; import { debounce } from 'App/utils'; import useSessionSearchQueryHandler from 'App/hooks/useSessionSearchQueryHandler'; -let debounceFetch: any = () => { -}; +let debounceFetch: () => void; function SessionSearch() { const { tagWatchStore, aiFiltersStore, searchStore, customFieldStore, projectsStore } = useStore(); const appliedFilter = searchStore.instance; const metaLoading = customFieldStore.isLoading; - const hasEvents = appliedFilter.filters.filter((i: any) => i.isEvent).length > 0; - const hasFilters = appliedFilter.filters.filter((i: any) => !i.isEvent).length > 0; + const hasEvents = appliedFilter.filters.some((i: any) => i.isEvent); + const hasFilters = appliedFilter.filters.some((i: any) => !i.isEvent); const saveRequestPayloads = projectsStore.instance?.saveRequestPayloads ?? false; useSessionSearchQueryHandler({ appliedFilter, loading: metaLoading, onBeforeLoad: async () => { - const tags = await tagWatchStore.getTags(); - if (tags) { - addOptionsToFilter( - FilterKey.TAGGED_ELEMENT, - tags.map((tag) => ({ - label: tag.name, - value: tag.tagId.toString() - })) - ); - searchStore.refreshFilterOptions(); + try { + const tags = await tagWatchStore.getTags(); + if (tags) { + addOptionsToFilter( + FilterKey.TAGGED_ELEMENT, + tags.map((tag) => ({ + label: tag.name, + value: tag.tagId.toString() + })) + ); + searchStore.refreshFilterOptions(); + } + } catch (error) { + console.error('Error during onBeforeLoad:', error); } } }); useEffect(() => { debounceFetch = debounce(() => searchStore.fetchSessions(), 500); - // void searchStore.fetchSessions(true) - }, []); + }, [searchStore]); useEffect(() => { debounceFetch(); @@ -85,49 +87,47 @@ function SessionSearch() { }; const showPanel = hasEvents || hasFilters || aiFiltersStore.isLoading; - return !metaLoading ? ( - <> - {showPanel ? ( -
-
- {aiFiltersStore.isLoading ? ( -
- - Translating your query into search steps... -
- ) : null} - {hasEvents || hasFilters ? ( - - ) : null} -
- {hasEvents || hasFilters ? ( -
-
- - - -
-
- -
-
- ) : null} + if (metaLoading) return null; + if (!showPanel) return null; + + return ( +
+
+ {aiFiltersStore.isLoading ? ( +
+ + Translating your query into search steps... +
+ ) : null} + {hasEvents || hasFilters ? ( + + ) : null} +
+ + {hasEvents || hasFilters ? ( +
+
+ + + +
+
+ +
- ) : ( - <> - )} - - ) : null; + ) : null} +
+ ); } export default observer(SessionSearch); diff --git a/frontend/app/hooks/useSessionSearchQueryHandler.ts b/frontend/app/hooks/useSessionSearchQueryHandler.ts index bbd26a26c..12511be38 100644 --- a/frontend/app/hooks/useSessionSearchQueryHandler.ts +++ b/frontend/app/hooks/useSessionSearchQueryHandler.ts @@ -6,46 +6,59 @@ import Search from '@/mstore/types/search'; import { getFilterFromJson } from 'Types/filter/newFilter'; interface Props { - onBeforeLoad?: () => Promise; - appliedFilter: any; + onBeforeLoad?: () => Promise; + appliedFilter: Record; loading: boolean; } -const useSessionSearchQueryHandler = (props: Props) => { +const useSessionSearchQueryHandler = ({ onBeforeLoad, appliedFilter, loading }: Props) => { const { searchStore } = useStore(); - const [beforeHookLoaded, setBeforeHookLoaded] = useState(!props.onBeforeLoad); - const { appliedFilter, loading } = props; + const [beforeHookLoaded, setBeforeHookLoaded] = useState(!onBeforeLoad); const history = useHistory(); + // Apply filter from the query string when the component mounts useEffect(() => { const applyFilterFromQuery = async () => { if (!loading && !searchStore.urlParsed) { - if (props.onBeforeLoad) { - await props.onBeforeLoad(); - setBeforeHookLoaded(true); - } + try { + if (onBeforeLoad) { + await onBeforeLoad(); + setBeforeHookLoaded(true); + } - const converter = JsonUrlConverter.urlParamsToJson(history.location.search); - const json: any = getFilterFromJson(converter.toJSON()); - const filter = new Search(json); - searchStore.applyFilter(filter, true); - searchStore.setUrlParsed() + const converter = JsonUrlConverter.urlParamsToJson(history.location.search); + const json = getFilterFromJson(converter.toJSON()); + const filter = new Search(json); + searchStore.applyFilter(filter, true); + searchStore.setUrlParsed(); + } catch (error) { + console.error('Error applying filter from query:', error); + } } }; void applyFilterFromQuery(); - }, [loading]); + }, [loading, onBeforeLoad, searchStore, history.location.search]); + // Update the URL whenever the appliedFilter changes useEffect(() => { - const generateUrlQuery = () => { + const updateUrlWithFilter = () => { if (!loading && beforeHookLoaded) { - const converter = JsonUrlConverter.jsonToUrlParams(appliedFilter); - history.replace({ search: converter }); + const query = JsonUrlConverter.jsonToUrlParams(appliedFilter); + history.replace({ search: query }); } }; - generateUrlQuery(); - }, [appliedFilter, loading, beforeHookLoaded]); + updateUrlWithFilter(); + }, [appliedFilter, loading, beforeHookLoaded, history]); + + // Ensure the URL syncs on remount if already parsed + useEffect(() => { + if (searchStore.urlParsed) { + const query = JsonUrlConverter.jsonToUrlParams(appliedFilter); + history.replace({ search: query }); + } + }, [appliedFilter, searchStore.urlParsed, history]); return null; }; diff --git a/frontend/app/mstore/types/search.ts b/frontend/app/mstore/types/search.ts index 4ba7ca2d6..ede994d61 100644 --- a/frontend/app/mstore/types/search.ts +++ b/frontend/app/mstore/types/search.ts @@ -1,7 +1,8 @@ -import { DATE_RANGE_VALUES, CUSTOM_RANGE, getDateRangeFromValue } from 'App/dateRange'; -import Filter, { checkFilterValue, IFilter } from 'App/mstore/types/filter'; +import { CUSTOM_RANGE, DATE_RANGE_VALUES, getDateRangeFromValue } from 'App/dateRange'; +import Filter, { IFilter } from 'App/mstore/types/filter'; import FilterItem from 'App/mstore/types/filterItem'; -import { action, makeAutoObservable, observable } from 'mobx'; +import { makeAutoObservable, observable } from 'mobx'; +import { LAST_24_HOURS, LAST_30_DAYS, LAST_7_DAYS } from 'Types/app/period'; // @ts-ignore const rangeValue = DATE_RANGE_VALUES.LAST_24_HOURS; @@ -69,7 +70,7 @@ export default class Search { constructor(initialData?: Partial) { makeAutoObservable(this, { - filters: observable, + filters: observable }); Object.assign(this, { name: '', @@ -142,11 +143,48 @@ export default class Search { return new FilterItem(filter).toJson(); }); + const { startDate, endDate } = this.getDateRange(js.rangeName, js.startDate, js.endDate); + js.startDate = startDate; + js.endDate = endDate; + delete js.createdAt; delete js.key; return js; } + private getDateRange(rangeName: string, customStartDate: number, customEndDate: number): { + startDate: number; + endDate: number + } { + let endDate = new Date().getTime(); + let startDate: number; + + switch (rangeName) { + case LAST_7_DAYS: + startDate = endDate - 7 * 24 * 60 * 60 * 1000; + break; + case LAST_30_DAYS: + startDate = endDate - 30 * 24 * 60 * 60 * 1000; + break; + case CUSTOM_RANGE: + if (!customStartDate || !customEndDate) { + throw new Error('Start date and end date must be provided for CUSTOM_RANGE.'); + } + startDate = customStartDate; + endDate = customEndDate; + break; + case LAST_24_HOURS: + default: + startDate = endDate - 24 * 60 * 60 * 1000; + } + + return { + startDate, + endDate + }; + } + + fromJS({ eventsOrder, filters, events, custom, ...filterData }: any) { let startDate, endDate; const rValue = filterData.rangeValue || rangeValue; @@ -176,3 +214,4 @@ export default class Search { }); } } + diff --git a/frontend/app/utils/search.ts b/frontend/app/utils/search.ts index 78f6e60b4..53357e41d 100644 --- a/frontend/app/utils/search.ts +++ b/frontend/app/utils/search.ts @@ -1,5 +1,4 @@ import Period, { CUSTOM_RANGE } from 'Types/app/period'; -import { filtersMap } from 'Types/filter/newFilter'; class Filter { @@ -25,24 +24,32 @@ class Filter { } } -class InputJson { +const DEFAULT_SORT = 'startTs'; +const DEFAULT_ORDER = 'desc'; +const DEFAULT_EVENTS_ORDER = 'then'; + +export class InputJson { filters: Filter[]; rangeValue: string; - startDate: number; - endDate: number; + startDate?: number; + endDate?: number; sort: string; order: string; eventsOrder: string; - constructor(filters: Filter[], rangeValue: string, startDate: number, endDate: number, sort: string, order: string, eventsOrder: string) { + constructor( + filters: Filter[], + rangeValue: string, + sort: string, + order: string, + eventsOrder: string, + startDate?: string | number, + endDate?: string | number + ) { this.filters = filters; - // .map((f: any) => { - // const subFilters = f.filters ? f.filters.map((sf: any) => new Filter(sf.key, sf.operator, sf.value, sf.filters)) : undefined; - // return new Filter(f.key, f.operator, f.value, subFilters); - // }); this.rangeValue = rangeValue; - this.startDate = startDate; - this.endDate = endDate; + this.startDate = startDate ? +startDate : undefined; + this.endDate = endDate ? +endDate : undefined; this.sort = sort; this.order = order; this.eventsOrder = eventsOrder; @@ -50,17 +57,28 @@ class InputJson { toJSON() { return { - filters: this.filters.map(f => f.toJSON()), + filters: this.filters.map((f) => f.toJSON()), rangeValue: this.rangeValue, - startDate: this.startDate, - endDate: this.endDate, + startDate: this.startDate ?? null, + endDate: this.endDate ?? null, sort: this.sort, order: this.order, eventsOrder: this.eventsOrder }; } -} + fromJSON(json: Record): InputJson { + return new InputJson( + json.filters.map((f: any) => new Filter(f.key, f.operator, f.value, f.filters)), + json.rangeValue, + json.sort, + json.order, + json.eventsOrder, + json.startDate, + json.endDate + ); + } +} export class JsonUrlConverter { static keyMap = { @@ -76,35 +94,46 @@ export class JsonUrlConverter { filters: 'f' }; - static getDateRangeValues(rangeValue: string, startDate: number | undefined, endDate: number | undefined): [number, number] { - if (rangeValue === 'CUSTOM_RANGE') { - return [startDate!, endDate!]; + static getDateRangeValues( + rangeValue: string, + startDate: string | null, + endDate: string | null + ): [string, string] { + if (rangeValue === CUSTOM_RANGE) { + return [startDate || '', endDate || '']; } - const period = Period({ rangeName: rangeValue }); + const period: any = Period({ rangeName: rangeValue }); return [period.start, period.end]; } - static jsonToUrlParams(json: InputJson): string { + static jsonToUrlParams(json: Record): string { const params = new URLSearchParams(); const addFilterParams = (filter: Filter, prefix: string) => { params.append(`${prefix}${this.keyMap.key}`, filter.key); params.append(`${prefix}${this.keyMap.operator}`, filter.operator); - if (filter.value) { - filter.value.forEach((v, i) => params.append(`${prefix}${this.keyMap.value}[${i}]`, v || '')); - } - if (filter.filters) { - filter.filters.forEach((f, i) => addFilterParams(f, `${prefix}${this.keyMap.filters}[${i}].`)); - } + filter.value?.forEach((v, i) => + params.append(`${prefix}${this.keyMap.value}[${i}]`, v || '') + ); + filter.filters?.forEach((f, i) => + addFilterParams(f, `${prefix}${this.keyMap.filters}[${i}].`) + ); }; - json.filters.forEach((filter, index) => addFilterParams(filter, `${this.keyMap.filters}[${index}].`)); - - const rangeValues = this.getDateRangeValues(json.rangeValue, json.startDate, json.endDate); + json.filters.forEach((filter: any, index: number) => + addFilterParams(filter, `${this.keyMap.filters}[${index}].`) + ); params.append(this.keyMap.rangeValue, json.rangeValue); - params.append(this.keyMap.startDate, rangeValues[0].toString()); - params.append(this.keyMap.endDate, rangeValues[1].toString()); + if (json.rangeValue === CUSTOM_RANGE) { + const rangeValues = this.getDateRangeValues( + json.rangeValue, + json.startDate?.toString() || null, + json.endDate?.toString() || null + ); + params.append(this.keyMap.startDate, rangeValues[0]); + params.append(this.keyMap.endDate, rangeValues[1]); + } params.append(this.keyMap.sort, json.sort); params.append(this.keyMap.order, json.order); params.append(this.keyMap.eventsOrder, json.eventsOrder); @@ -130,7 +159,7 @@ export class JsonUrlConverter { filters.push(getFilterParams(`${prefix}${this.keyMap.filters}[${index}].`)); index++; } - return new Filter(key, operator, value.length ? value : '', filters.length ? filters : []); + return new Filter(key, operator, value.length ? value : [], filters.length ? filters : []); }; const filters: Filter[] = []; @@ -142,21 +171,20 @@ export class JsonUrlConverter { const rangeValue = params.get(this.keyMap.rangeValue) || 'LAST_24_HOURS'; const rangeValues = this.getDateRangeValues(rangeValue, params.get(this.keyMap.startDate), params.get(this.keyMap.endDate)); - const startDate = rangeValues[0]; - const endDate = rangeValues[1]; return new InputJson( filters, rangeValue, - startDate, - endDate, - params.get(this.keyMap.sort) || 'startTs', - params.get(this.keyMap.order) || 'desc', - params.get(this.keyMap.eventsOrder) || 'then' + params.get(this.keyMap.sort) || DEFAULT_SORT, + params.get(this.keyMap.order) || DEFAULT_ORDER, + params.get(this.keyMap.eventsOrder) || DEFAULT_EVENTS_ORDER, + rangeValues[0], + rangeValues[1] ); } } + // Example usage // const urlParams = '?f[0].k=click&f[0].op=on&f[0].v[0]=Refresh&f[1].k=fetch&f[1].op=is&f[1].v[0]=&f[1].f[0].k=fetchUrl&f[1].f[0].op=is&f[1].f[0].v[0]=/g/collect&f[1].f[1].k=fetchStatusCode&f[1].f[1].op=>=&f[1].f[1].v[0]=400&f[1].f[2].k=fetchMethod&f[1].f[2].op=is&f[1].f[2].v[0]=&f[1].f[3].k=fetchDuration&f[1].f[3].op==&f[1].f[3].v[0]=&f[1].f[4].k=fetchRequestBody&f[1].f[4].op=is&f[1].f[4].v[0]=&f[1].f[5].k=fetchResponseBody&f[1].f[5].op=is&f[1].f[5].v[0]=&rv=LAST_24_HOURS&sd=1731343412555&ed=1731429812555&s=startTs&o=desc&st=false&eo=then'; // const parsedJson = JsonUrlConverter.urlParamsToJson(urlParams);