fix(ui): sessions, bookmark, notes navigation and search silters and timestamp issues
This commit is contained in:
parent
253feefe53
commit
f5df3fb5b5
11 changed files with 137 additions and 100 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>
|
||||
|
|
|
|||
|
|
@ -45,9 +45,10 @@ function SessionSearch() {
|
|||
|
||||
useEffect(() => {
|
||||
debounceFetch = debounce(() => searchStore.fetchSessions(), 500);
|
||||
}, [searchStore]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchStore.urlParsed) return;
|
||||
debounceFetch();
|
||||
}, [appliedFilter.filters]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ export default class Search {
|
|||
return new FilterItem(filter).toJson();
|
||||
});
|
||||
|
||||
const { startDate, endDate } = this.getDateRange(js.rangeName, js.startDate, js.endDate);
|
||||
const { startDate, endDate } = this.getDateRange(js.rangeValue, js.startDate, js.endDate);
|
||||
js.startDate = startDate;
|
||||
js.endDate = endDate;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import Period, { CUSTOM_RANGE } from 'Types/app/period';
|
||||
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;
|
||||
|
|
@ -24,10 +27,6 @@ class Filter {
|
|||
}
|
||||
}
|
||||
|
||||
const DEFAULT_SORT = 'startTs';
|
||||
const DEFAULT_ORDER = 'desc';
|
||||
const DEFAULT_EVENTS_ORDER = 'then';
|
||||
|
||||
export class InputJson {
|
||||
filters: Filter[];
|
||||
rangeValue: string;
|
||||
|
|
@ -169,7 +168,7 @@ 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));
|
||||
|
||||
return new InputJson(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue