fix(ui) - sessions refresh and navigation (#2785)
* change(ui): search query params improvements * fix(ui): sessions, bookmark, notes navigation and search silters and timestamp issues
This commit is contained in:
parent
251d727375
commit
f0e8100283
12 changed files with 330 additions and 213 deletions
|
|
@ -12,6 +12,8 @@ import { withRouter, RouteComponentProps, useLocation } from 'react-router-dom';
|
|||
import FlagView from 'Components/FFlags/FlagView/FlagView';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from '@/mstore';
|
||||
import NotesList from 'Shared/SessionsTabOverview/components/Notes/NoteList';
|
||||
import NoteTags from 'Shared/SessionsTabOverview/components/Notes/NoteTags';
|
||||
|
||||
// @ts-ignore
|
||||
interface IProps extends RouteComponentProps {
|
||||
|
|
@ -36,15 +38,16 @@ function Overview({ match: { params } }: IProps) {
|
|||
return (
|
||||
<Switch>
|
||||
<Route exact strict
|
||||
path={[withSiteId(sessions(), siteId), withSiteId(notes(), siteId), withSiteId(bookmarks(), siteId)]}>
|
||||
path={[withSiteId(sessions(), siteId), withSiteId(bookmarks(), siteId)]}>
|
||||
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
|
||||
<NoSessionsMessage siteId={siteId} />
|
||||
<MainSearchBar />
|
||||
<SessionSearch />
|
||||
<div className="my-4" />
|
||||
<SessionsTabOverview />
|
||||
</div>
|
||||
</Route>
|
||||
<Route exact strict path={withSiteId(notes(), siteId)}>
|
||||
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
|
||||
<NotesList />
|
||||
</div>
|
||||
</Route>
|
||||
<Route exact strict path={withSiteId(fflags(), siteId)}>
|
||||
<FFlagsList siteId={siteId} />
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -11,41 +11,44 @@ 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)
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchStore.urlParsed) return;
|
||||
debounceFetch();
|
||||
}, [appliedFilter.filters]);
|
||||
|
||||
|
|
@ -85,49 +88,47 @@ function SessionSearch() {
|
|||
};
|
||||
|
||||
const showPanel = hasEvents || hasFilters || aiFiltersStore.isLoading;
|
||||
return !metaLoading ? (
|
||||
<>
|
||||
{showPanel ? (
|
||||
<div className="border bg-white rounded-lg mt-4">
|
||||
<div className="p-5">
|
||||
{aiFiltersStore.isLoading ? (
|
||||
<div className={'font-semibold flex items-center gap-2 mb-2'}>
|
||||
<AnimatedSVG name={ICONS.LOADER} size={18} />
|
||||
<span>Translating your query into search steps...</span>
|
||||
</div>
|
||||
) : null}
|
||||
{hasEvents || hasFilters ? (
|
||||
<FilterList
|
||||
filter={appliedFilter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
onFilterMove={onFilterMove}
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{hasEvents || hasFilters ? (
|
||||
<div className="border-t px-5 py-1 flex items-center -mx-2">
|
||||
<div>
|
||||
<FilterSelection filter={undefined} onFilterClick={onAddFilter}>
|
||||
<Button variant="text-primary" className="mr-2" icon="plus">
|
||||
ADD STEP
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<SaveFilterButton />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
if (metaLoading) return null;
|
||||
if (!showPanel) return null;
|
||||
|
||||
return (
|
||||
<div className="border bg-white rounded-lg mt-4">
|
||||
<div className="p-5">
|
||||
{aiFiltersStore.isLoading ? (
|
||||
<div className={'font-semibold flex items-center gap-2 mb-2'}>
|
||||
<AnimatedSVG name={ICONS.LOADER} size={18} />
|
||||
<span>Translating your query into search steps...</span>
|
||||
</div>
|
||||
) : null}
|
||||
{hasEvents || hasFilters ? (
|
||||
<FilterList
|
||||
filter={appliedFilter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
onFilterMove={onFilterMove}
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{hasEvents || hasFilters ? (
|
||||
<div className="border-t px-5 py-1 flex items-center -mx-2">
|
||||
<div>
|
||||
<FilterSelection filter={undefined} onFilterClick={onAddFilter}>
|
||||
<Button variant="text-primary" className="mr-2" icon="plus">
|
||||
ADD STEP
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<SaveFilterButton />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
) : null;
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(SessionSearch);
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@ import React from 'react';
|
|||
import { useStore } from 'App/mstore';
|
||||
|
||||
import LatestSessionsMessage from './components/LatestSessionsMessage';
|
||||
import NotesList from './components/Notes/NoteList';
|
||||
import SessionHeader from './components/SessionHeader';
|
||||
import SessionList from './components/SessionList';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import NoSessionsMessage from 'Shared/NoSessionsMessage/NoSessionsMessage';
|
||||
import MainSearchBar from 'Shared/MainSearchBar/MainSearchBar';
|
||||
import SessionSearch from 'Shared/SessionSearch/SessionSearch';
|
||||
|
||||
function SessionsTabOverview() {
|
||||
const [query, setQuery] = React.useState('');
|
||||
const { aiFiltersStore, searchStore } = useStore();
|
||||
const appliedFilter = searchStore.instance;
|
||||
const isNotesRoute = searchStore.activeTab.type === 'notes';
|
||||
|
||||
const handleKeyDown = (event: any) => {
|
||||
if (event.key === 'Enter') {
|
||||
|
|
@ -25,27 +26,27 @@ function SessionsTabOverview() {
|
|||
|
||||
const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true';
|
||||
return (
|
||||
<div className="widget-wrapper">
|
||||
{testingKey ? (
|
||||
<Input
|
||||
value={query}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className={'mb-2'}
|
||||
placeholder={'ask session ai'}
|
||||
/>
|
||||
) : null}
|
||||
<SessionHeader />
|
||||
<div className="border-b" />
|
||||
{!isNotesRoute ? (
|
||||
<>
|
||||
<LatestSessionsMessage />
|
||||
<SessionList />
|
||||
</>
|
||||
) : (
|
||||
<NotesList />
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<NoSessionsMessage />
|
||||
<MainSearchBar />
|
||||
<SessionSearch />
|
||||
<div className="my-4" />
|
||||
<div className="widget-wrapper">
|
||||
{testingKey ? (
|
||||
<Input
|
||||
value={query}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className={'mb-2'}
|
||||
placeholder={'ask session ai'}
|
||||
/>
|
||||
) : null}
|
||||
<SessionHeader />
|
||||
<div className="border-b" />
|
||||
<LatestSessionsMessage />
|
||||
<SessionList />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,57 +5,70 @@ import NoteItem from './NoteItem';
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import NoteTags from 'Shared/SessionsTabOverview/components/Notes/NoteTags';
|
||||
|
||||
function NotesList() {
|
||||
const { notesStore } = useStore();
|
||||
|
||||
React.useEffect(() => {
|
||||
void notesStore.fetchNotes();
|
||||
void notesStore.fetchNotes();
|
||||
}, [notesStore.page]);
|
||||
|
||||
const list = notesStore.notes;
|
||||
|
||||
return (
|
||||
<Loader loading={notesStore.loading}>
|
||||
<NoContent
|
||||
show={list.length === 0}
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
{/* <Icon name="no-dashboard" size={80} color="figmaColors-accent-secondary" /> */}
|
||||
<AnimatedSVG name={ICONS.NO_NOTES} size={60} />
|
||||
<div className="text-center mt-4 text-lg font-medium">No notes yet</div>
|
||||
</div>
|
||||
}
|
||||
subtext={
|
||||
<div className="text-center flex justify-center items-center flex-col">
|
||||
Note observations during session replays and share them with your team.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="border-b rounded bg-white">
|
||||
{list.map((note) => (
|
||||
<React.Fragment key={note.noteId}>
|
||||
<NoteItem note={note} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
<div className="widget-wrapper">
|
||||
<div className="flex items-center px-4 py-1 justify-between w-full">
|
||||
<h2 className="text-2xl capitalize mr-4">Notes</h2>
|
||||
|
||||
<div className="w-full flex items-center justify-between py-4 px-6">
|
||||
<div className="text-disabled-text">
|
||||
Showing{' '}
|
||||
<span className="font-semibold">{Math.min(list.length, notesStore.pageSize)}</span> out
|
||||
of <span className="font-semibold">{notesStore.total}</span> notes
|
||||
<div className="flex items-center justify-end w-full">
|
||||
<NoteTags />
|
||||
</div>
|
||||
<Pagination
|
||||
page={notesStore.page}
|
||||
total={notesStore.total}
|
||||
onPageChange={(page) => notesStore.changePage(page)}
|
||||
limit={notesStore.pageSize}
|
||||
debounceRequest={100}
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
<div className="border-b" />
|
||||
<Loader loading={notesStore.loading}>
|
||||
<NoContent
|
||||
show={list.length === 0}
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
{/* <Icon name="no-dashboard" size={80} color="figmaColors-accent-secondary" /> */}
|
||||
<AnimatedSVG name={ICONS.NO_NOTES} size={60} />
|
||||
<div className="text-center mt-4 text-lg font-medium">No notes yet</div>
|
||||
</div>
|
||||
}
|
||||
subtext={
|
||||
<div className="text-center flex justify-center items-center flex-col">
|
||||
Note observations during session replays and share them with your team.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="border-b rounded bg-white">
|
||||
{list.map((note) => (
|
||||
<React.Fragment key={note.noteId}>
|
||||
<NoteItem note={note} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full flex items-center justify-between py-4 px-6">
|
||||
<div className="text-disabled-text">
|
||||
Showing{' '}
|
||||
<span className="font-semibold">{Math.min(list.length, notesStore.pageSize)}</span> out
|
||||
of <span className="font-semibold">{notesStore.total}</span> notes
|
||||
</div>
|
||||
<Pagination
|
||||
page={notesStore.page}
|
||||
total={notesStore.total}
|
||||
onPageChange={(page) => notesStore.changePage(page)}
|
||||
limit={notesStore.pageSize}
|
||||
debounceRequest={100}
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, { useMemo } from 'react';
|
|||
import Period from 'Types/app/period';
|
||||
import SelectDateRange from 'Shared/SelectDateRange';
|
||||
import SessionTags from '../SessionTags';
|
||||
import NoteTags from '../Notes/NoteTags';
|
||||
import SessionSort from '../SessionSort';
|
||||
import { Space } from 'antd';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
|
@ -17,9 +16,6 @@ function SessionHeader() {
|
|||
const period = Period({ start: startDate, end: endDate, rangeName: rangeValue });
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (activeTab.type === 'notes') {
|
||||
return 'Notes';
|
||||
}
|
||||
if (activeTab.type === 'bookmarks') {
|
||||
return isEnterprise ? 'Vault' : 'Bookmarks';
|
||||
}
|
||||
|
|
@ -35,26 +31,15 @@ function SessionHeader() {
|
|||
return (
|
||||
<div className="flex items-center px-4 py-1 justify-between w-full">
|
||||
<h2 className="text-2xl capitalize mr-4">{title}</h2>
|
||||
{activeTab.type !== 'notes' ? (
|
||||
<div className="flex items-center w-full justify-end">
|
||||
{activeTab.type !== 'bookmarks' && (
|
||||
<>
|
||||
<SessionTags />
|
||||
<div className="mr-auto" />
|
||||
<Space>
|
||||
<SelectDateRange isAnt period={period} onChange={onDateChange} right={true} />
|
||||
<SessionSort />
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{activeTab.type === 'notes' && (
|
||||
<div className="flex items-center justify-end w-full">
|
||||
<NoteTags />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center w-full justify-end">
|
||||
{activeTab.type !== 'bookmarks' && <SessionTags />}
|
||||
<div className="mr-auto" />
|
||||
<Space>
|
||||
{activeTab.type !== 'bookmarks' &&
|
||||
<SelectDateRange isAnt period={period} onChange={onDateChange} right={true} />}
|
||||
<SessionSort />
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ function SessionList() {
|
|||
}, [isBookmark, isVault, activeTab, location.pathname]);
|
||||
const [statusData, setStatusData] = React.useState<SessionStatus>({ status: 0, count: 0 });
|
||||
|
||||
|
||||
const fetchStatus = async () => {
|
||||
const response = await sessionService.getRecordingStatus();
|
||||
setStatusData({
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
import { DateTime, Duration } from 'luxon'; // TODO
|
||||
import { Timezone } from 'App/mstore/types/sessionSettings';
|
||||
import { LAST_24_HOURS, LAST_30_DAYS, LAST_7_DAYS } from 'Types/app/period';
|
||||
import { CUSTOM_RANGE } from '@/dateRange';
|
||||
|
||||
export function getDateFromString(date: string, format = 'yyyy-MM-dd HH:mm:ss:SSS'): string {
|
||||
return DateTime.fromISO(date).toFormat(format);
|
||||
|
|
@ -191,3 +193,35 @@ export const countDaysFrom = (timestamp: number): number => {
|
|||
const d = new Date();
|
||||
return Math.round(Math.abs(d.getTime() - date.toJSDate().getTime()) / (1000 * 3600 * 24));
|
||||
}
|
||||
|
||||
export const getDateRangeUTC = (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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,46 +6,59 @@ import Search from '@/mstore/types/search';
|
|||
import { getFilterFromJson } from 'Types/filter/newFilter';
|
||||
|
||||
interface Props {
|
||||
onBeforeLoad?: () => Promise<any>;
|
||||
appliedFilter: any;
|
||||
onBeforeLoad?: () => Promise<void>;
|
||||
appliedFilter: Record<string, any>;
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export default class FilterStore {
|
|||
}
|
||||
|
||||
setTopValues = (key: string, values: TopValue[]) => {
|
||||
this.topValues[key] = values.filter((value) => value !== null && value.value !== '');
|
||||
this.topValues[key] = values?.filter((value) => value !== null && value.value !== '');
|
||||
};
|
||||
|
||||
fetchTopValues = async (key: string, source?: string) => {
|
||||
|
|
|
|||
|
|
@ -264,6 +264,8 @@ class SearchStore {
|
|||
});
|
||||
}
|
||||
|
||||
this.currentPage = 1;
|
||||
|
||||
if (filter.value && filter.value[0] && filter.value[0] !== '') {
|
||||
void this.fetchSessions();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ISearch>) {
|
||||
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.rangeValue, 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 {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import Period, { CUSTOM_RANGE } from 'Types/app/period';
|
||||
import { filtersMap } from 'Types/filter/newFilter';
|
||||
import Period, { CUSTOM_RANGE, LAST_24_HOURS } from 'Types/app/period';
|
||||
|
||||
const DEFAULT_SORT = 'startTs';
|
||||
const DEFAULT_ORDER = 'desc';
|
||||
const DEFAULT_EVENTS_ORDER = 'then';
|
||||
|
||||
class Filter {
|
||||
key: string;
|
||||
|
|
@ -25,24 +27,28 @@ class Filter {
|
|||
}
|
||||
}
|
||||
|
||||
class InputJson {
|
||||
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 +56,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<string, any>): 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 +93,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, any>): 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 +158,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[] = [];
|
||||
|
|
@ -140,23 +168,22 @@ export class JsonUrlConverter {
|
|||
index++;
|
||||
}
|
||||
|
||||
const rangeValue = params.get(this.keyMap.rangeValue) || 'LAST_24_HOURS';
|
||||
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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue