Compare commits

...
Sign in to create a new pull request.

15 commits

Author SHA1 Message Date
Shekar Siri
0bbc055db9 fix(ui): sessions, bookmark, notes navigation and search silters and timestamp issues 2024-11-26 12:58:51 +01:00
Shekar Siri
fa65f8be41 change(ui): search query params improvements 2024-11-26 12:58:44 +01:00
Mehdi Osman
251d727375
Increment chalice chart version (#2783)
Co-authored-by: GitHub Action <action@github.com>
2024-11-25 17:42:22 +01:00
Kraiem Taha Yassine
b00a90484e
fix(chalice): support user-city for assist (#2782) 2024-11-25 17:35:51 +01:00
Mehdi Osman
ce0686eec3
Increment frontend chart version (#2780)
Co-authored-by: GitHub Action <action@github.com>
2024-11-25 14:49:59 +01:00
Shekar Siri
34232ed23c
Fix sessions list (#2779)
* fix(ui): sessions list persist page, show latest sessions

* fix(ui): latest sessions check clear the list
2024-11-25 13:43:07 +01:00
Mehdi Osman
954bfbf8f7
Increment frontend chart version (#2778)
Co-authored-by: GitHub Action <action@github.com>
2024-11-25 12:22:33 +01:00
Shekar Siri
c0197cdfeb
fix(ui): sessions list persist page, show latest sessions (#2777) 2024-11-25 12:12:16 +01:00
Andrés Carrillo
12ab110e0e
Update common.env (#2776) 2024-11-23 10:24:31 -05:00
Mehdi Osman
f48808f42e
Increment frontend chart version (#2774)
Co-authored-by: GitHub Action <action@github.com>
2024-11-22 14:15:28 +01:00
Delirium
b080a98764
ui: fix ws panel crash (#2773) 2024-11-22 14:07:42 +01:00
Mehdi Osman
dd885c65ac
Increment frontend chart version (#2770)
Co-authored-by: GitHub Action <action@github.com>
2024-11-20 15:35:32 +01:00
Shekar Siri
0ad2836650
cherry-pick(ui): 54abbe58a (#2769) 2024-11-20 14:02:42 +01:00
Mehdi Osman
20b76a0ed9
Updated patch build from main 884f3499ef (#2768)
* Increment chalice chart version

* Increment alerts chart version

---------

Co-authored-by: GitHub Action <action@github.com>
2024-11-20 12:45:49 +01:00
Kraiem Taha Yassine
884f3499ef
fix(chalice): support top graphql autocomplete (#2767)
refactor(chalice): enforce UTC TZ
refactor(crons): enforce UTC TZ
refactor(alerts): enforce UTC TZ
2024-11-20 12:34:25 +01:00
29 changed files with 369 additions and 232 deletions

View file

@ -1,3 +1,4 @@
#!/bin/sh #!/bin/sh
export TZ=UTC
uvicorn app:app --host 0.0.0.0 --port $LISTEN_PORT --proxy-headers --log-level ${S_LOGLEVEL:-warning} uvicorn app:app --host 0.0.0.0 --port $LISTEN_PORT --proxy-headers --log-level ${S_LOGLEVEL:-warning}

View file

@ -1,3 +1,4 @@
#!/bin/sh #!/bin/sh
export TZ=UTC
export ASSIST_KEY=ignore export ASSIST_KEY=ignore
uvicorn app:app --host 0.0.0.0 --port 8888 --log-level ${S_LOGLEVEL:-warning} uvicorn app:app --host 0.0.0.0 --port 8888 --log-level ${S_LOGLEVEL:-warning}

View file

@ -1,3 +1,4 @@
#!/bin/zsh #!/bin/zsh
export TZ=UTC
uvicorn app_alerts:app --reload --port 8888 --log-level ${S_LOGLEVEL:-warning} uvicorn app_alerts:app --reload --port 8888 --log-level ${S_LOGLEVEL:-warning}

View file

@ -1,3 +1,4 @@
#!/bin/zsh #!/bin/zsh
export TZ=UTC
uvicorn app:app --reload --log-level ${S_LOGLEVEL:-warning} uvicorn app:app --reload --log-level ${S_LOGLEVEL:-warning}

View file

@ -1357,6 +1357,7 @@ class LiveFilterType(str, Enum):
USER_BROWSER = FilterType.USER_BROWSER.value USER_BROWSER = FilterType.USER_BROWSER.value
USER_DEVICE = FilterType.USER_DEVICE.value USER_DEVICE = FilterType.USER_DEVICE.value
USER_COUNTRY = FilterType.USER_COUNTRY.value USER_COUNTRY = FilterType.USER_COUNTRY.value
USER_CITY = FilterType.USER_CITY.value
USER_STATE = FilterType.USER_STATE.value USER_STATE = FilterType.USER_STATE.value
USER_ID = FilterType.USER_ID.value USER_ID = FilterType.USER_ID.value
USER_ANONYMOUS_ID = FilterType.USER_ANONYMOUS_ID.value USER_ANONYMOUS_ID = FilterType.USER_ANONYMOUS_ID.value

View file

@ -58,6 +58,7 @@ def get_event_type(event_type: Union[schemas.EventType, schemas.PerformanceEvent
schemas.EventType.REQUEST: "REQUEST", schemas.EventType.REQUEST: "REQUEST",
schemas.EventType.REQUEST_DETAILS: "REQUEST", schemas.EventType.REQUEST_DETAILS: "REQUEST",
schemas.PerformanceEventType.FETCH_FAILED: "REQUEST", schemas.PerformanceEventType.FETCH_FAILED: "REQUEST",
schemas.GraphqlFilterType.GRAPHQL_NAME: "GRAPHQL",
schemas.EventType.STATE_ACTION: "STATEACTION", schemas.EventType.STATE_ACTION: "STATEACTION",
schemas.EventType.ERROR: "ERROR", schemas.EventType.ERROR: "ERROR",
schemas.PerformanceEventType.LOCATION_AVG_CPU_LOAD: 'PERFORMANCE', schemas.PerformanceEventType.LOCATION_AVG_CPU_LOAD: 'PERFORMANCE',

View file

@ -1,4 +1,5 @@
#!/bin/sh #!/bin/sh
export TZ=UTC
sh env_vars.sh sh env_vars.sh
source /tmp/.env.override source /tmp/.env.override

View file

@ -1,4 +1,5 @@
#!/bin/sh #!/bin/sh
export TZ=UTC
export ASSIST_KEY=ignore export ASSIST_KEY=ignore
sh env_vars.sh sh env_vars.sh
source /tmp/.env.override source /tmp/.env.override

View file

@ -1,4 +1,5 @@
#!/bin/sh #!/bin/sh
export TZ=UTC
export ASSIST_KEY=ignore export ASSIST_KEY=ignore
sh env_vars.sh sh env_vars.sh
source /tmp/.env.override source /tmp/.env.override

View file

@ -12,6 +12,8 @@ import { withRouter, RouteComponentProps, useLocation } from 'react-router-dom';
import FlagView from 'Components/FFlags/FlagView/FlagView'; import FlagView from 'Components/FFlags/FlagView/FlagView';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useStore } from '@/mstore'; import { useStore } from '@/mstore';
import NotesList from 'Shared/SessionsTabOverview/components/Notes/NoteList';
import NoteTags from 'Shared/SessionsTabOverview/components/Notes/NoteTags';
// @ts-ignore // @ts-ignore
interface IProps extends RouteComponentProps { interface IProps extends RouteComponentProps {
@ -36,15 +38,16 @@ function Overview({ match: { params } }: IProps) {
return ( return (
<Switch> <Switch>
<Route exact strict <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' }}> <div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
<NoSessionsMessage siteId={siteId} />
<MainSearchBar />
<SessionSearch />
<div className="my-4" />
<SessionsTabOverview /> <SessionsTabOverview />
</div> </div>
</Route> </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)}> <Route exact strict path={withSiteId(fflags(), siteId)}>
<FFlagsList siteId={siteId} /> <FFlagsList siteId={siteId} />
</Route> </Route>

View file

@ -23,9 +23,9 @@ const lineLength = 40;
function WSPanel({ socketMsgList, onClose }: Props) { function WSPanel({ socketMsgList, onClose }: Props) {
const [query, setQuery] = React.useState(''); const [query, setQuery] = React.useState('');
const [list, setList] = React.useState(socketMsgList); const [list, setList] = React.useState(socketMsgList);
const [selectedRow, setSelectedRow] = React.useState<SocketMsg | null>(null); const [selectedRow, setSelectedRow] = React.useState<{ msg: SocketMsg, id: number } | null>(null);
const onQueryChange = (e) => { const onQueryChange = (e: any) => {
setQuery(e.target.value); setQuery(e.target.value);
const newList = filterList(socketMsgList, e.target.value, [ const newList = filterList(socketMsgList, e.target.value, [
'data', 'data',
@ -69,15 +69,16 @@ function WSPanel({ socketMsgList, onClose }: Props) {
position: 'relative', position: 'relative',
}} }}
> >
{list.map((msg) => ( {list.map((msg, i) => (
<Row <Row
msg={msg} msg={msg}
key={msg.timestamp} key={msg.timestamp}
onSelect={() => setSelectedRow(msg)} onSelect={() => setSelectedRow({ msg, id: i })}
isSelected={selectedRow ? selectedRow.id === i : false}
/> />
))} ))}
{selectedRow ? ( {selectedRow ? (
<SelectedRow msg={selectedRow} onClose={() => setSelectedRow(null)} /> <SelectedRow msg={selectedRow.msg} onClose={() => setSelectedRow(null)} />
) : null} ) : null}
</div> </div>
</div> </div>
@ -127,7 +128,7 @@ function MsgDirection({ dir }: { dir: 'up' | 'down' }) {
); );
} }
function Row({ msg, onSelect }: { msg: SocketMsg; onSelect: () => void }) { function Row({ msg, onSelect, isSelected }: { msg: SocketMsg; isSelected: boolean; onSelect: () => void }) {
return ( return (
<> <>
<div <div
@ -149,7 +150,7 @@ function Row({ msg, onSelect }: { msg: SocketMsg; onSelect: () => void }) {
'rounded-full font-bold text-xl p-2 bg-white w-6 h-6 flex items-center justify-center' 'rounded-full font-bold text-xl p-2 bg-white w-6 h-6 flex items-center justify-center'
} }
> >
<span>{isOpen ? '-' : '+'}</span> <span>{isSelected ? '-' : '+'}</span>
</div> </div>
) : null} ) : null}
</div> </div>

View file

@ -11,41 +11,44 @@ import { useStore } from 'App/mstore';
import { debounce } from 'App/utils'; import { debounce } from 'App/utils';
import useSessionSearchQueryHandler from 'App/hooks/useSessionSearchQueryHandler'; import useSessionSearchQueryHandler from 'App/hooks/useSessionSearchQueryHandler';
let debounceFetch: any = () => { let debounceFetch: () => void;
};
function SessionSearch() { function SessionSearch() {
const { tagWatchStore, aiFiltersStore, searchStore, customFieldStore, projectsStore } = useStore(); const { tagWatchStore, aiFiltersStore, searchStore, customFieldStore, projectsStore } = useStore();
const appliedFilter = searchStore.instance; const appliedFilter = searchStore.instance;
const metaLoading = customFieldStore.isLoading; const metaLoading = customFieldStore.isLoading;
const hasEvents = appliedFilter.filters.filter((i: any) => i.isEvent).length > 0; const hasEvents = appliedFilter.filters.some((i: any) => i.isEvent);
const hasFilters = appliedFilter.filters.filter((i: any) => !i.isEvent).length > 0; const hasFilters = appliedFilter.filters.some((i: any) => !i.isEvent);
const saveRequestPayloads = projectsStore.instance?.saveRequestPayloads ?? false; const saveRequestPayloads = projectsStore.instance?.saveRequestPayloads ?? false;
useSessionSearchQueryHandler({ useSessionSearchQueryHandler({
appliedFilter, appliedFilter,
loading: metaLoading, loading: metaLoading,
onBeforeLoad: async () => { onBeforeLoad: async () => {
const tags = await tagWatchStore.getTags(); try {
if (tags) { const tags = await tagWatchStore.getTags();
addOptionsToFilter( if (tags) {
FilterKey.TAGGED_ELEMENT, addOptionsToFilter(
tags.map((tag) => ({ FilterKey.TAGGED_ELEMENT,
label: tag.name, tags.map((tag) => ({
value: tag.tagId.toString() label: tag.name,
})) value: tag.tagId.toString()
); }))
searchStore.refreshFilterOptions(); );
searchStore.refreshFilterOptions();
}
} catch (error) {
console.error('Error during onBeforeLoad:', error);
} }
} }
}); });
useEffect(() => { useEffect(() => {
debounceFetch = debounce(() => searchStore.fetchSessions(), 500); debounceFetch = debounce(() => searchStore.fetchSessions(), 500);
// void searchStore.fetchSessions(true)
}, []); }, []);
useEffect(() => { useEffect(() => {
if (searchStore.urlParsed) return;
debounceFetch(); debounceFetch();
}, [appliedFilter.filters]); }, [appliedFilter.filters]);
@ -85,49 +88,47 @@ function SessionSearch() {
}; };
const showPanel = hasEvents || hasFilters || aiFiltersStore.isLoading; 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 ? ( if (metaLoading) return null;
<div className="border-t px-5 py-1 flex items-center -mx-2"> if (!showPanel) return null;
<div>
<FilterSelection filter={undefined} onFilterClick={onAddFilter}> return (
<Button variant="text-primary" className="mr-2" icon="plus"> <div className="border bg-white rounded-lg mt-4">
ADD STEP <div className="p-5">
</Button> {aiFiltersStore.isLoading ? (
</FilterSelection> <div className={'font-semibold flex items-center gap-2 mb-2'}>
</div> <AnimatedSVG name={ICONS.LOADER} size={18} />
<div className="ml-auto flex items-center"> <span>Translating your query into search steps...</span>
<SaveFilterButton /> </div>
</div> ) : null}
</div> {hasEvents || hasFilters ? (
) : null} <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> </div>
) : ( ) : null}
<></> </div>
)} );
</>
) : null;
} }
export default observer(SessionSearch); export default observer(SessionSearch);

View file

@ -3,16 +3,17 @@ import React from 'react';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
import LatestSessionsMessage from './components/LatestSessionsMessage'; import LatestSessionsMessage from './components/LatestSessionsMessage';
import NotesList from './components/Notes/NoteList';
import SessionHeader from './components/SessionHeader'; import SessionHeader from './components/SessionHeader';
import SessionList from './components/SessionList'; import SessionList from './components/SessionList';
import { observer } from 'mobx-react-lite'; 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() { function SessionsTabOverview() {
const [query, setQuery] = React.useState(''); const [query, setQuery] = React.useState('');
const { aiFiltersStore, searchStore } = useStore(); const { aiFiltersStore, searchStore } = useStore();
const appliedFilter = searchStore.instance; const appliedFilter = searchStore.instance;
const isNotesRoute = searchStore.activeTab.type === 'notes';
const handleKeyDown = (event: any) => { const handleKeyDown = (event: any) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
@ -25,25 +26,27 @@ function SessionsTabOverview() {
const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true'; const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true';
return ( return (
<div className="widget-wrapper"> <>
{testingKey ? ( <NoSessionsMessage />
<Input <MainSearchBar />
value={query} <SessionSearch />
onKeyDown={handleKeyDown} <div className="my-4" />
onChange={(e) => setQuery(e.target.value)} <div className="widget-wrapper">
className={'mb-2'} {testingKey ? (
placeholder={'ask session ai'} <Input
/> value={query}
) : null} onKeyDown={handleKeyDown}
<SessionHeader /> onChange={(e) => setQuery(e.target.value)}
<div className="border-b" /> className={'mb-2'}
<LatestSessionsMessage /> placeholder={'ask session ai'}
{!isNotesRoute ? ( />
) : null}
<SessionHeader />
<div className="border-b" />
<LatestSessionsMessage />
<SessionList /> <SessionList />
) : ( </div>
<NotesList /> </>
)}
</div>
); );
} }

View file

@ -7,11 +7,16 @@ import { observer } from 'mobx-react-lite';
function LatestSessionsMessage() { function LatestSessionsMessage() {
const { searchStore } = useStore(); const { searchStore } = useStore();
const count = searchStore.latestList.size; const count = searchStore.latestList.size;
const onShowNewSessions = () => {
void searchStore.updateCurrentPage(1, true);
};
return count > 0 ? ( return count > 0 ? (
<div <div
className="bg-amber-50 p-1 flex w-full border-b text-center justify-center link" className="bg-amber-50 p-1 flex w-full border-b text-center justify-center link"
style={{ backgroundColor: 'rgb(255 251 235)' }} style={{ backgroundColor: 'rgb(255 251 235)' }}
onClick={() => searchStore.updateCurrentPage(1)} onClick={onShowNewSessions}
> >
Show {numberWithCommas(count)} New {count > 1 ? 'Sessions' : 'Session'} Show {numberWithCommas(count)} New {count > 1 ? 'Sessions' : 'Session'}
</div> </div>

View file

@ -5,57 +5,70 @@ import NoteItem from './NoteItem';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import NoteTags from 'Shared/SessionsTabOverview/components/Notes/NoteTags';
function NotesList() { function NotesList() {
const { notesStore } = useStore(); const { notesStore } = useStore();
React.useEffect(() => { React.useEffect(() => {
void notesStore.fetchNotes(); void notesStore.fetchNotes();
}, [notesStore.page]); }, [notesStore.page]);
const list = notesStore.notes; const list = notesStore.notes;
return ( return (
<Loader loading={notesStore.loading}> <>
<NoContent <div className="widget-wrapper">
show={list.length === 0} <div className="flex items-center px-4 py-1 justify-between w-full">
title={ <h2 className="text-2xl capitalize mr-4">Notes</h2>
<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="flex items-center justify-end w-full">
<div className="text-disabled-text"> <NoteTags />
Showing{' '}
<span className="font-semibold">{Math.min(list.length, notesStore.pageSize)}</span> out
of <span className="font-semibold">{notesStore.total}</span> notes
</div> </div>
<Pagination
page={notesStore.page}
total={notesStore.total}
onPageChange={(page) => notesStore.changePage(page)}
limit={notesStore.pageSize}
debounceRequest={100}
/>
</div> </div>
</NoContent> <div className="border-b" />
</Loader> <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>
</>
); );
} }

View file

@ -2,7 +2,6 @@ import React, { useMemo } from 'react';
import Period from 'Types/app/period'; import Period from 'Types/app/period';
import SelectDateRange from 'Shared/SelectDateRange'; import SelectDateRange from 'Shared/SelectDateRange';
import SessionTags from '../SessionTags'; import SessionTags from '../SessionTags';
import NoteTags from '../Notes/NoteTags';
import SessionSort from '../SessionSort'; import SessionSort from '../SessionSort';
import { Space } from 'antd'; import { Space } from 'antd';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
@ -17,9 +16,6 @@ function SessionHeader() {
const period = Period({ start: startDate, end: endDate, rangeName: rangeValue }); const period = Period({ start: startDate, end: endDate, rangeName: rangeValue });
const title = useMemo(() => { const title = useMemo(() => {
if (activeTab.type === 'notes') {
return 'Notes';
}
if (activeTab.type === 'bookmarks') { if (activeTab.type === 'bookmarks') {
return isEnterprise ? 'Vault' : 'Bookmarks'; return isEnterprise ? 'Vault' : 'Bookmarks';
} }
@ -35,26 +31,15 @@ function SessionHeader() {
return ( return (
<div className="flex items-center px-4 py-1 justify-between w-full"> <div className="flex items-center px-4 py-1 justify-between w-full">
<h2 className="text-2xl capitalize mr-4">{title}</h2> <h2 className="text-2xl capitalize mr-4">{title}</h2>
{activeTab.type !== 'notes' ? ( <div className="flex items-center w-full justify-end">
<div className="flex items-center w-full justify-end"> {activeTab.type !== 'bookmarks' && <SessionTags />}
{activeTab.type !== 'bookmarks' && ( <div className="mr-auto" />
<> <Space>
<SessionTags /> {activeTab.type !== 'bookmarks' &&
<div className="mr-auto" /> <SelectDateRange isAnt period={period} onChange={onDateChange} right={true} />}
<Space> <SessionSort />
<SelectDateRange isAnt period={period} onChange={onDateChange} right={true} /> </Space>
<SessionSort /> </div>
</Space>
</>
)}
</div>
) : null}
{activeTab.type === 'notes' && (
<div className="flex items-center justify-end w-full">
<NoteTags />
</div>
)}
</div> </div>
); );
} }

View file

@ -73,7 +73,6 @@ function SessionList() {
}, [isBookmark, isVault, activeTab, location.pathname]); }, [isBookmark, isVault, activeTab, location.pathname]);
const [statusData, setStatusData] = React.useState<SessionStatus>({ status: 0, count: 0 }); const [statusData, setStatusData] = React.useState<SessionStatus>({ status: 0, count: 0 });
const fetchStatus = async () => { const fetchStatus = async () => {
const response = await sessionService.getRecordingStatus(); const response = await sessionService.getRecordingStatus();
setStatusData({ setStatusData({

View file

@ -2,6 +2,8 @@
import { DateTime, Duration } from 'luxon'; // TODO import { DateTime, Duration } from 'luxon'; // TODO
import { Timezone } from 'App/mstore/types/sessionSettings'; 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 { export function getDateFromString(date: string, format = 'yyyy-MM-dd HH:mm:ss:SSS'): string {
return DateTime.fromISO(date).toFormat(format); return DateTime.fromISO(date).toFormat(format);
@ -191,3 +193,35 @@ export const countDaysFrom = (timestamp: number): number => {
const d = new Date(); const d = new Date();
return Math.round(Math.abs(d.getTime() - date.toJSDate().getTime()) / (1000 * 3600 * 24)); 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
};
}

View file

@ -6,46 +6,59 @@ import Search from '@/mstore/types/search';
import { getFilterFromJson } from 'Types/filter/newFilter'; import { getFilterFromJson } from 'Types/filter/newFilter';
interface Props { interface Props {
onBeforeLoad?: () => Promise<any>; onBeforeLoad?: () => Promise<void>;
appliedFilter: any; appliedFilter: Record<string, any>;
loading: boolean; loading: boolean;
} }
const useSessionSearchQueryHandler = (props: Props) => { const useSessionSearchQueryHandler = ({ onBeforeLoad, appliedFilter, loading }: Props) => {
const { searchStore } = useStore(); const { searchStore } = useStore();
const [beforeHookLoaded, setBeforeHookLoaded] = useState(!props.onBeforeLoad); const [beforeHookLoaded, setBeforeHookLoaded] = useState(!onBeforeLoad);
const { appliedFilter, loading } = props;
const history = useHistory(); const history = useHistory();
// Apply filter from the query string when the component mounts
useEffect(() => { useEffect(() => {
const applyFilterFromQuery = async () => { const applyFilterFromQuery = async () => {
if (!loading && !searchStore.urlParsed) { if (!loading && !searchStore.urlParsed) {
if (props.onBeforeLoad) { try {
await props.onBeforeLoad(); if (onBeforeLoad) {
setBeforeHookLoaded(true); await onBeforeLoad();
} setBeforeHookLoaded(true);
}
const converter = JsonUrlConverter.urlParamsToJson(history.location.search); const converter = JsonUrlConverter.urlParamsToJson(history.location.search);
const json: any = getFilterFromJson(converter.toJSON()); const json = getFilterFromJson(converter.toJSON());
const filter = new Search(json); const filter = new Search(json);
searchStore.applyFilter(filter, true); searchStore.applyFilter(filter, true);
searchStore.setUrlParsed() searchStore.setUrlParsed();
} catch (error) {
console.error('Error applying filter from query:', error);
}
} }
}; };
void applyFilterFromQuery(); void applyFilterFromQuery();
}, [loading]); }, [loading, onBeforeLoad, searchStore, history.location.search]);
// Update the URL whenever the appliedFilter changes
useEffect(() => { useEffect(() => {
const generateUrlQuery = () => { const updateUrlWithFilter = () => {
if (!loading && beforeHookLoaded) { if (!loading && beforeHookLoaded) {
const converter = JsonUrlConverter.jsonToUrlParams(appliedFilter); const query = JsonUrlConverter.jsonToUrlParams(appliedFilter);
history.replace({ search: converter }); history.replace({ search: query });
} }
}; };
generateUrlQuery(); updateUrlWithFilter();
}, [appliedFilter, loading, beforeHookLoaded]); }, [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; return null;
}; };

View file

@ -20,7 +20,7 @@ export default class FilterStore {
} }
setTopValues = (key: string, values: TopValue[]) => { 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) => { fetchTopValues = async (key: string, source?: string) => {

View file

@ -162,15 +162,14 @@ class SearchStore {
}); });
} }
updateCurrentPage(page: number) { updateCurrentPage(page: number, force = false) {
this.currentPage = page; this.currentPage = page;
void this.fetchSessions(); void this.fetchSessions(force);
} }
setActiveTab(tab: string) { setActiveTab(tab: string) {
runInAction(() => { runInAction(() => {
this.activeTab = TAB_MAP[tab]; this.activeTab = TAB_MAP[tab];
this.currentPage = 1;
}); });
} }
@ -229,12 +228,13 @@ class SearchStore {
if (this.latestRequestTime) { if (this.latestRequestTime) {
const period = Period({ rangeName: CUSTOM_RANGE, start: this.latestRequestTime, end: Date.now() }); const period = Period({ rangeName: CUSTOM_RANGE, start: this.latestRequestTime, end: Date.now() });
const newTimestamps: any = period.toJSON(); const newTimestamps: any = period.toJSON();
filter.startTimestamp = newTimestamps.startDate; filter.startDate = newTimestamps.startDate;
filter.endTimestamp = newTimestamps.endDate; filter.endDate = newTimestamps.endDate;
} }
searchService.checkLatestSessions(filter).then((response: any) => { searchService.checkLatestSessions(filter).then((response: any) => {
this.latestList = response; runInAction(() => {
this.latestRequestTime = Date.now(); this.latestList = List(response);
});
}); });
} }
@ -264,8 +264,10 @@ class SearchStore {
}); });
} }
this.currentPage = 1;
if (filter.value && filter.value[0] && filter.value[0] !== '') { if (filter.value && filter.value[0] && filter.value[0] !== '') {
this.fetchSessions(); void this.fetchSessions();
} }
} }
@ -336,6 +338,9 @@ class SearchStore {
filter.filters = filter.filters.concat(tagFilter); filter.filters = filter.filters.concat(tagFilter);
} }
this.latestRequestTime = Date.now();
this.latestList = List();
await sessionStore.fetchSessions({ await sessionStore.fetchSessions({
...filter, ...filter,
page: this.currentPage, page: this.currentPage,

View file

@ -170,7 +170,7 @@ export default class SessionStore {
} }
const nextEntryNum = const nextEntryNum =
keys.length > 0 keys.length > 0
? Math.max(...keys.map((key) => this.prefetchedMobUrls[key].entryNum || 0)) + 1 ? Math.max(...keys.map((key) => this.prefetchedMobUrls[key]?.entryNum || 0)) + 1
: 0; : 0;
this.prefetchedMobUrls[sessionId] = { this.prefetchedMobUrls[sessionId] = {
data: fileData, data: fileData,

View file

@ -1,7 +1,8 @@
import { DATE_RANGE_VALUES, CUSTOM_RANGE, getDateRangeFromValue } from 'App/dateRange'; import { CUSTOM_RANGE, DATE_RANGE_VALUES, getDateRangeFromValue } from 'App/dateRange';
import Filter, { checkFilterValue, IFilter } from 'App/mstore/types/filter'; import Filter, { IFilter } from 'App/mstore/types/filter';
import FilterItem from 'App/mstore/types/filterItem'; 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 // @ts-ignore
const rangeValue = DATE_RANGE_VALUES.LAST_24_HOURS; const rangeValue = DATE_RANGE_VALUES.LAST_24_HOURS;
@ -69,7 +70,7 @@ export default class Search {
constructor(initialData?: Partial<ISearch>) { constructor(initialData?: Partial<ISearch>) {
makeAutoObservable(this, { makeAutoObservable(this, {
filters: observable, filters: observable
}); });
Object.assign(this, { Object.assign(this, {
name: '', name: '',
@ -142,11 +143,48 @@ export default class Search {
return new FilterItem(filter).toJson(); 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.createdAt;
delete js.key; delete js.key;
return js; 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) { fromJS({ eventsOrder, filters, events, custom, ...filterData }: any) {
let startDate, endDate; let startDate, endDate;
const rValue = filterData.rangeValue || rangeValue; const rValue = filterData.rangeValue || rangeValue;
@ -176,3 +214,4 @@ export default class Search {
}); });
} }
} }

View file

@ -1,6 +1,8 @@
import Period, { CUSTOM_RANGE } from 'Types/app/period'; import Period, { CUSTOM_RANGE, LAST_24_HOURS } from 'Types/app/period';
import { filtersMap } from 'Types/filter/newFilter';
const DEFAULT_SORT = 'startTs';
const DEFAULT_ORDER = 'desc';
const DEFAULT_EVENTS_ORDER = 'then';
class Filter { class Filter {
key: string; key: string;
@ -25,24 +27,28 @@ class Filter {
} }
} }
class InputJson { export class InputJson {
filters: Filter[]; filters: Filter[];
rangeValue: string; rangeValue: string;
startDate: number; startDate?: number;
endDate: number; endDate?: number;
sort: string; sort: string;
order: string; order: string;
eventsOrder: 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; 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.rangeValue = rangeValue;
this.startDate = startDate; this.startDate = startDate ? +startDate : undefined;
this.endDate = endDate; this.endDate = endDate ? +endDate : undefined;
this.sort = sort; this.sort = sort;
this.order = order; this.order = order;
this.eventsOrder = eventsOrder; this.eventsOrder = eventsOrder;
@ -50,17 +56,28 @@ class InputJson {
toJSON() { toJSON() {
return { return {
filters: this.filters.map(f => f.toJSON()), filters: this.filters.map((f) => f.toJSON()),
rangeValue: this.rangeValue, rangeValue: this.rangeValue,
startDate: this.startDate, startDate: this.startDate ?? null,
endDate: this.endDate, endDate: this.endDate ?? null,
sort: this.sort, sort: this.sort,
order: this.order, order: this.order,
eventsOrder: this.eventsOrder 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 { export class JsonUrlConverter {
static keyMap = { static keyMap = {
@ -76,35 +93,46 @@ export class JsonUrlConverter {
filters: 'f' filters: 'f'
}; };
static getDateRangeValues(rangeValue: string, startDate: number | undefined, endDate: number | undefined): [number, number] { static getDateRangeValues(
if (rangeValue === 'CUSTOM_RANGE') { rangeValue: string,
return [startDate!, endDate!]; 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]; return [period.start, period.end];
} }
static jsonToUrlParams(json: InputJson): string { static jsonToUrlParams(json: Record<string, any>): string {
const params = new URLSearchParams(); const params = new URLSearchParams();
const addFilterParams = (filter: Filter, prefix: string) => { const addFilterParams = (filter: Filter, prefix: string) => {
params.append(`${prefix}${this.keyMap.key}`, filter.key); params.append(`${prefix}${this.keyMap.key}`, filter.key);
params.append(`${prefix}${this.keyMap.operator}`, filter.operator); params.append(`${prefix}${this.keyMap.operator}`, filter.operator);
if (filter.value) { filter.value?.forEach((v, i) =>
filter.value.forEach((v, i) => params.append(`${prefix}${this.keyMap.value}[${i}]`, v || '')); params.append(`${prefix}${this.keyMap.value}[${i}]`, v || '')
} );
if (filter.filters) { filter.filters?.forEach((f, i) =>
filter.filters.forEach((f, i) => addFilterParams(f, `${prefix}${this.keyMap.filters}[${i}].`)); addFilterParams(f, `${prefix}${this.keyMap.filters}[${i}].`)
} );
}; };
json.filters.forEach((filter, index) => addFilterParams(filter, `${this.keyMap.filters}[${index}].`)); json.filters.forEach((filter: any, index: number) =>
addFilterParams(filter, `${this.keyMap.filters}[${index}].`)
const rangeValues = this.getDateRangeValues(json.rangeValue, json.startDate, json.endDate); );
params.append(this.keyMap.rangeValue, json.rangeValue); params.append(this.keyMap.rangeValue, json.rangeValue);
params.append(this.keyMap.startDate, rangeValues[0].toString()); if (json.rangeValue === CUSTOM_RANGE) {
params.append(this.keyMap.endDate, rangeValues[1].toString()); 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.sort, json.sort);
params.append(this.keyMap.order, json.order); params.append(this.keyMap.order, json.order);
params.append(this.keyMap.eventsOrder, json.eventsOrder); params.append(this.keyMap.eventsOrder, json.eventsOrder);
@ -130,7 +158,7 @@ export class JsonUrlConverter {
filters.push(getFilterParams(`${prefix}${this.keyMap.filters}[${index}].`)); filters.push(getFilterParams(`${prefix}${this.keyMap.filters}[${index}].`));
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[] = []; const filters: Filter[] = [];
@ -140,23 +168,22 @@ export class JsonUrlConverter {
index++; 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 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( return new InputJson(
filters, filters,
rangeValue, rangeValue,
startDate, params.get(this.keyMap.sort) || DEFAULT_SORT,
endDate, params.get(this.keyMap.order) || DEFAULT_ORDER,
params.get(this.keyMap.sort) || 'startTs', params.get(this.keyMap.eventsOrder) || DEFAULT_EVENTS_ORDER,
params.get(this.keyMap.order) || 'desc', rangeValues[0],
params.get(this.keyMap.eventsOrder) || 'then' rangeValues[1]
); );
} }
} }
// Example usage // 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 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); // const parsedJson = JsonUrlConverter.urlParamsToJson(urlParams);

View file

@ -26,7 +26,7 @@
"@babel/plugin-transform-private-methods": "^7.23.3", "@babel/plugin-transform-private-methods": "^7.23.3",
"@floating-ui/react-dom-interactions": "^0.10.3", "@floating-ui/react-dom-interactions": "^0.10.3",
"@medv/finder": "^3.1.0", "@medv/finder": "^3.1.0",
"@sentry/browser": "^5.21.1", "@sentry/browser": "^8.34.0",
"@svg-maps/world": "^1.0.1", "@svg-maps/world": "^1.0.1",
"@tanstack/react-query": "^5.56.2", "@tanstack/react-query": "^5.56.2",
"@wojtekmaj/react-daterange-picker": "^6.0.0", "@wojtekmaj/react-daterange-picker": "^6.0.0",

View file

@ -9,7 +9,7 @@ COMMON_PG_PASSWORD="change_me_pg_password"
COMMON_VERSION="v1.16.0" COMMON_VERSION="v1.16.0"
## DB versions ## DB versions
###################################### ######################################
POSTGRES_VERSION="14.5.0" POSTGRES_VERSION="15.10.0"
REDIS_VERSION="6.0.12-debian-10-r33" REDIS_VERSION="6.0.12-debian-10-r33"
MINIO_VERSION="2023.2.10-debian-11-r1" MINIO_VERSION="2023.2.10-debian-11-r1"
###################################### ######################################

View file

@ -18,4 +18,4 @@ version: 0.1.1
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
AppVersion: "v1.21.0" AppVersion: "v1.21.1"

View file

@ -18,4 +18,4 @@ version: 0.1.7
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
AppVersion: "v1.21.0" AppVersion: "v1.21.2"

View file

@ -18,4 +18,4 @@ version: 0.1.10
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
AppVersion: "v1.21.0" AppVersion: "v1.21.4"