diff --git a/frontend/app/components/Assist/components/AssistTabs/AssistTabs.tsx b/frontend/app/components/Assist/components/AssistTabs/AssistTabs.tsx index 93a73c236..2fd09105b 100644 --- a/frontend/app/components/Assist/components/AssistTabs/AssistTabs.tsx +++ b/frontend/app/components/Assist/components/AssistTabs/AssistTabs.tsx @@ -20,7 +20,7 @@ const AssistTabs = (props: Props) => { <>
showModal(, {})} + onClick={() => showModal(, { right: true })} > Active Sessions
diff --git a/frontend/app/components/BugFinder/SessionList/SessionListHeader.js b/frontend/app/components/BugFinder/SessionList/SessionListHeader.js index afb135099..c6994fdaf 100644 --- a/frontend/app/components/BugFinder/SessionList/SessionListHeader.js +++ b/frontend/app/components/BugFinder/SessionList/SessionListHeader.js @@ -26,7 +26,6 @@ function SessionListHeader({ activeTab, count, applyFilter, filter }) { }, [label]); const { startDate, endDate, rangeValue } = filter; - console.log('startDate', startDate); const period = new Record({ start: startDate, end: endDate, rangeName: rangeValue, timezoneOffset: getTimeZoneOffset() }); const onDateChange = (e) => { diff --git a/frontend/app/components/Modal/ModalOverlay.tsx b/frontend/app/components/Modal/ModalOverlay.tsx index 5b2a9edab..0a56646b8 100644 --- a/frontend/app/components/Modal/ModalOverlay.tsx +++ b/frontend/app/components/Modal/ModalOverlay.tsx @@ -4,7 +4,7 @@ import cn from 'classnames'; function ModalOverlay({ hideModal, children, left = false, right = false }: any) { return ( -
+
{children}
diff --git a/frontend/app/components/Session/Session.js b/frontend/app/components/Session/Session.js index d6bf31a53..ec8b42196 100644 --- a/frontend/app/components/Session/Session.js +++ b/frontend/app/components/Session/Session.js @@ -9,6 +9,7 @@ import { sessions as sessionsRoute } from 'App/routes'; import withPermissions from 'HOCs/withPermissions' import WebPlayer from './WebPlayer'; import IOSPlayer from './IOSPlayer'; +import { useStore } from 'App/mstore'; const SESSIONS_ROUTE = sessionsRoute(); @@ -22,6 +23,7 @@ function Session({ }) { usePageTitle("OpenReplay Session Player"); const [ initializing, setInitializing ] = useState(true) + const { sessionStore } = useStore(); useEffect(() => { if (sessionId != null) { fetchSession(sessionId) @@ -31,6 +33,10 @@ function Session({ setInitializing(false) },[ sessionId ]); + useEffect(() => { + sessionStore.resetUserFilter(); + } ,[]) + return ( { - setShowUserSessions(true); - request({ key: !userId ? 'USERANONYMOUSID' : 'USERID', value: userId || userAnonymousId }); - } + const hasUserDetails = !!userId || !!userAnonymousId; + const showSimilarSessions = () => { + setShowUserSessions(true); + request({ key: !userId ? 'USERANONYMOUSID' : 'USERID', value: userId || userAnonymousId }); + }; - const getDimension = (width, height) => { - return width && height ? ( -
- { width || 'x' } { height || 'x' } -
- ) : Resolution N/A; - } + const getDimension = (width, height) => { + return width && height ? ( +
+ {width || 'x'} {height || 'x'} +
+ ) : ( + Resolution N/A + ); + }; - const avatarbgSize = '38px' - return ( -
-
- -
- - { userDisplayName } - + const avatarbgSize = '38px'; + return ( +
+
+ +
+ + + -
- {formatTimeOrDate(startedAt, timezone)} - · - {countries[userCountry]} - · - {userBrowser}, {userOs}, {userDevice} - · - - } label={countries[userCountry]} value={{formatTimeOrDate(startedAt)} } /> - - - - {revId && } -
- )} - position="bottom center" - hoverable - disabled={false} - on="hover" - > - - More - - -
-
-
+
+ {formatTimeOrDate(startedAt, timezone)} + · + {countries[userCountry]} + · + + {userBrowser}, {userOs}, {userDevice} + + · + + } + label={countries[userCountry]} + value={{formatTimeOrDate(startedAt)}} + /> + + + + {revId && } +
+ } + position="bottom center" + hoverable + disabled={false} + on="hover" + > + More + +
+
+
- User Sessions
} isDisplayed={ showUserSessions } content={ showUserSessions && } onClose={ () => showUserSessions ? setShowUserSessions(false) : null } - /> -
- ) + /> */} + + ); } -const component = React.memo(connect(state => ({ session: state.getIn([ 'sessions', 'current' ]) }))(UserCard)) +const component = React.memo(connect((state) => ({ session: state.getIn(['sessions', 'current']) }))(UserCard)); export default withRequest({ - initialData: List(), - endpoint: '/metadata/session_search', - dataWrapper: data => Object.values(data), - dataName: 'similarSessions', -})(component) + initialData: List(), + endpoint: '/metadata/session_search', + dataWrapper: (data) => Object.values(data), + dataName: 'similarSessions', +})(component); + +// inner component +function UserName({ name, userId, hash }) { + const { showModal } = useModal(); + const onClick = () => { + showModal(, { right: true }); + }; + return
{}}>{name}
; +} diff --git a/frontend/app/components/shared/SessionItem/SessionItem.tsx b/frontend/app/components/shared/SessionItem/SessionItem.tsx index 83d2e6bb3..e22f7e532 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.tsx +++ b/frontend/app/components/shared/SessionItem/SessionItem.tsx @@ -52,6 +52,7 @@ interface Props { lastPlayedSessionId?: string; live?: boolean; onClick?: any; + compact?: boolean; } function SessionItem(props: RouteComponentProps & Props) { @@ -67,6 +68,7 @@ function SessionItem(props: RouteComponentProps & Props) { metaList = [], lastPlayedSessionId, onClick = null, + compact = false, } = props; const { @@ -109,24 +111,26 @@ function SessionItem(props: RouteComponentProps & Props) {
e.stopPropagation()}>
-
-
- -
-
-
!disableUser && !hasUserFilter && onUserClick(userId, userAnonymousId)} - > - + {!compact && ( +
+
+ +
+
+
!disableUser && !hasUserFilter && onUserClick(userId, userAnonymousId)} + > + +
-
-
+ )} +
diff --git a/frontend/app/components/shared/UserSessionsModal/UserSessionsModal.tsx b/frontend/app/components/shared/UserSessionsModal/UserSessionsModal.tsx new file mode 100644 index 000000000..fac7d1b41 --- /dev/null +++ b/frontend/app/components/shared/UserSessionsModal/UserSessionsModal.tsx @@ -0,0 +1,93 @@ +import React, { useEffect } from 'react'; +import { useStore } from 'App/mstore'; +import Filter from 'Types/filter'; +import { filtersMap } from 'Types/filter/newFilter'; +import { FilterKey } from 'App/types/filter/filterType'; +import { NoContent, Pagination, Loader, Avatar } from 'UI'; +import SessionItem from 'Shared/SessionItem'; +import SelectDateRange from 'Shared/SelectDateRange'; +import Period from 'Types/app/period'; +import { useObserver, observer } from 'mobx-react-lite'; + +const PER_PAGE = 10; +interface Props { + userId: string; + hash: string; + name: string; +} +function UserSessionsModal(props: Props) { + const { userId, hash, name } = props; + const { sessionStore } = useStore(); + const [loading, setLoading] = React.useState(false); + const [data, setData] = React.useState({ sessions: [], total: 0 }); + const filter = useObserver(() => sessionStore.userFilter); + + const onDateChange = (period: any) => { + filter.update('period', period); + }; + + const fetchData = () => { + setLoading(true); + sessionStore + .getSessions(filter) + .then(setData) + .catch(() => { + console.log('error'); + }) + .finally(() => { + setLoading(false); + }); + }; + + useEffect(() => { + const userFilter = { key: FilterKey.USERID, value: [userId], operator: 'is', isEvent: false }; + filter.update('filters', [userFilter]); + }, []); + useEffect(fetchData, [filter.page, filter.startDate, filter.endDate]); + + return ( +
+
+
+ +
+ {name}'s Sessions +
+
+
+ +
+
+ + No recordings found.
}> +
+ + {data.sessions.map((session: any) => ( +
+ +
+ ))} +
+ +
+
+ {/* showing x to x of total sessions */} + Showing {(filter.page - 1) * PER_PAGE + 1} to{' '} + {(filter.page - 1) * PER_PAGE + data.sessions.length} of{' '} + {data.total} sessions. +
+ filter.update('page', page)} + limit={PER_PAGE} + debounceRequest={1000} + /> +
+
+ +
+ ); +} + +export default observer(UserSessionsModal); diff --git a/frontend/app/components/shared/UserSessionsModal/index.ts b/frontend/app/components/shared/UserSessionsModal/index.ts new file mode 100644 index 000000000..c48e3ab5a --- /dev/null +++ b/frontend/app/components/shared/UserSessionsModal/index.ts @@ -0,0 +1 @@ +export { default } from './UserSessionsModal'; \ No newline at end of file diff --git a/frontend/app/duck/search.js b/frontend/app/duck/search.js index 8859ace22..5876abbb0 100644 --- a/frontend/app/duck/search.js +++ b/frontend/app/duck/search.js @@ -288,9 +288,9 @@ export function fetchFilterSearch(params) { } export const clearSearch = () => (dispatch, getState) => { - const filter = getState().getIn(['search', 'instance']); + // const filter = getState().getIn(['search', 'instance']); // dispatch(applySavedSearch(new SavedFilter({}))); - dispatch(edit(new Filter({ startDate: filter.startDate, endDate: filter.endDate, rangeValue: filter.rangeValue, filters: [] }))); + dispatch(edit(new Filter({ filters: [] }))); return dispatch({ type: CLEAR_SEARCH, }); diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index 445a174f3..0d7c5ce5d 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -10,6 +10,7 @@ import SettingsStore from './settingsStore'; import AuditStore from './auditStore'; import NotificationStore from './notificationStore'; import ErrorStore from './errorStore'; +import SessionStore from './sessionStore'; export class RootStore { dashboardStore: IDashboardStore; @@ -21,6 +22,7 @@ export class RootStore { auditStore: AuditStore; errorStore: ErrorStore; notificationStore: NotificationStore + sessionStore: SessionStore; constructor() { this.dashboardStore = new DashboardStore(); @@ -32,6 +34,7 @@ export class RootStore { this.auditStore = new AuditStore(); this.errorStore = new ErrorStore(); this.notificationStore = new NotificationStore(); + this.sessionStore = new SessionStore(); } initClient() { diff --git a/frontend/app/mstore/sessionStore.ts b/frontend/app/mstore/sessionStore.ts new file mode 100644 index 000000000..98a7061e6 --- /dev/null +++ b/frontend/app/mstore/sessionStore.ts @@ -0,0 +1,79 @@ +import { makeAutoObservable, observable, action } from 'mobx'; +import { sessionService } from 'App/services'; +import { filterMap } from 'Duck/search'; +import Session from './types/session'; +import Record, { LAST_7_DAYS } from 'Types/app/period'; + +class UserFilter { + endDate: number = new Date().getTime(); + startDate: number = new Date().getTime() - 24 * 60 * 60 * 1000; + rangeName: string = LAST_7_DAYS; + filters: any = []; + page: number = 1; + limit: number = 10; + period: any = Record({ rangeName: LAST_7_DAYS }); + + constructor() { + makeAutoObservable(this, { + page: observable, + update: action, + }); + } + + update(key: string, value: any) { + this[key] = value; + + if (key === 'period') { + this.startDate = this.period.start; + this.endDate = this.period.end; + } + } + + setFilters(filters: any[]) { + this.filters = filters; + } + + setPage(page: number) { + this.page = page; + } + + toJson() { + return { + endDate: this.period.end, + startDate: this.period.start, + filters: this.filters.map(filterMap), + page: this.page, + limit: this.limit, + }; + } +} + +export default class SessionStore { + userFilter: UserFilter = new UserFilter(); + + constructor() { + makeAutoObservable(this, { + userFilter: observable, + }); + } + + resetUserFilter() { + this.userFilter = new UserFilter(); + } + + getSessions(filter: any): Promise { + return new Promise((resolve, reject) => { + sessionService + .getSessions(filter.toJson()) + .then((response: any) => { + resolve({ + sessions: response.sessions.map((session: any) => new Session().fromJson(session)), + total: response.total, + }); + }) + .catch((error: any) => { + reject(error); + }); + }); + } +} diff --git a/frontend/app/services/SessionService.ts b/frontend/app/services/SessionService.ts index a7940edc1..07c623359 100644 --- a/frontend/app/services/SessionService.ts +++ b/frontend/app/services/SessionService.ts @@ -1,4 +1,5 @@ import APIClient from 'App/api_client'; +import { fetchErrorCheck } from 'App/utils'; export default class SettingsService { private client: APIClient; @@ -16,8 +17,16 @@ export default class SettingsService { } fetchCaptureRate() { - return this.client.get('/sample_rate') - .then(response => response.json()) - .then(response => response.data || 0); + return this.client + .get('/sample_rate') + .then((response) => response.json()) + .then((response) => response.data || 0); } -} \ No newline at end of file + + getSessions(filter: any) { + return this.client + .post('/sessions/search2', filter) + .then(fetchErrorCheck) + .then((response) => response.data || []); + } +}