feat ui: prefetch mobfiles on hover, use prefetched if exist (#2098)
This commit is contained in:
parent
156e1f94d6
commit
ceb714617e
11 changed files with 440 additions and 204 deletions
|
|
@ -1,17 +1,18 @@
|
|||
import withPermissions from 'HOCs/withPermissions';
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import usePageTitle from 'App/hooks/usePageTitle';
|
||||
import { fetchV2, clearCurrentSession } from "Duck/sessions";
|
||||
import { fetchList as fetchSlackList } from 'Duck/integrations/slack';
|
||||
import { Link, NoContent, Loader } from 'UI';
|
||||
import { sessions as sessionsRoute } from 'App/routes';
|
||||
import withPermissions from 'HOCs/withPermissions'
|
||||
import WebPlayer from './WebPlayer';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { clearLogs } from 'App/dev/console';
|
||||
|
||||
import MobilePlayer from "Components/Session/MobilePlayer";
|
||||
import { clearLogs } from 'App/dev/console';
|
||||
import usePageTitle from 'App/hooks/usePageTitle';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { sessions as sessionsRoute } from 'App/routes';
|
||||
import MobilePlayer from 'Components/Session/MobilePlayer';
|
||||
import { fetchList as fetchSlackList } from 'Duck/integrations/slack';
|
||||
import { clearCurrentSession, fetchV2 } from 'Duck/sessions';
|
||||
import { Link, Loader, NoContent } from 'UI';
|
||||
|
||||
import WebPlayer from './WebPlayer';
|
||||
|
||||
const SESSIONS_ROUTE = sessionsRoute();
|
||||
|
||||
|
|
@ -26,61 +27,73 @@ interface Props {
|
|||
|
||||
function Session({
|
||||
sessionId,
|
||||
loading,
|
||||
hasErrors,
|
||||
fetchV2,
|
||||
clearCurrentSession,
|
||||
session,
|
||||
}: Props) {
|
||||
usePageTitle("OpenReplay Session Player");
|
||||
const [ initializing, setInitializing ] = useState(true)
|
||||
}: Props) {
|
||||
usePageTitle('OpenReplay Session Player');
|
||||
const { sessionStore } = useStore();
|
||||
useEffect(() => {
|
||||
if (sessionId != null) {
|
||||
fetchV2(sessionId)
|
||||
fetchV2(sessionId);
|
||||
} else {
|
||||
console.error("No sessionID in route.")
|
||||
console.error('No sessionID in route.');
|
||||
}
|
||||
setInitializing(false)
|
||||
return () => {
|
||||
clearCurrentSession();
|
||||
}
|
||||
},[ sessionId ]);
|
||||
};
|
||||
}, [sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
clearLogs()
|
||||
clearLogs();
|
||||
sessionStore.resetUserFilter();
|
||||
} ,[])
|
||||
}, []);
|
||||
|
||||
const player = session.isMobileNative ? <MobilePlayer /> : <WebPlayer />
|
||||
const player = session.isMobileNative ? <MobilePlayer /> : <WebPlayer />;
|
||||
return (
|
||||
<NoContent
|
||||
show={ hasErrors }
|
||||
show={hasErrors}
|
||||
title="Session not found."
|
||||
subtext={
|
||||
<span>
|
||||
{'Please check your data retention plan, or try '}
|
||||
<Link to={ SESSIONS_ROUTE } className="link">{'another one'}</Link>
|
||||
<Link to={SESSIONS_ROUTE} className="link">
|
||||
{'another one'}
|
||||
</Link>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Loader className="flex-1" loading={ loading || initializing }>
|
||||
<Loader className="flex-1" loading={!session.sessionId}>
|
||||
{player}
|
||||
</Loader>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default withPermissions(['SESSION_REPLAY'], '', true)(connect((state: any, props: any) => {
|
||||
const { match: { params: { sessionId } } } = props;
|
||||
export default withPermissions(
|
||||
['SESSION_REPLAY'],
|
||||
'',
|
||||
true
|
||||
)(
|
||||
connect(
|
||||
(state: any, props: any) => {
|
||||
const {
|
||||
match: {
|
||||
params: { sessionId },
|
||||
},
|
||||
} = props;
|
||||
return {
|
||||
sessionId,
|
||||
loading: state.getIn([ 'sessions', 'loading' ]),
|
||||
hasErrors: !!state.getIn([ 'sessions', 'errors' ]),
|
||||
session: state.getIn([ 'sessions', 'current' ]),
|
||||
loading: state.getIn(['sessions', 'loading']),
|
||||
hasErrors: !!state.getIn(['sessions', 'errors']),
|
||||
session: state.getIn(['sessions', 'current']),
|
||||
};
|
||||
}, {
|
||||
},
|
||||
{
|
||||
fetchSlackList,
|
||||
fetchV2,
|
||||
clearCurrentSession,
|
||||
})(Session));
|
||||
}
|
||||
)(Session)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,21 +1,28 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Modal, Loader } from 'UI';
|
||||
import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player';
|
||||
import { fetchList } from 'Duck/integrations';
|
||||
import withLocationHandlers from 'HOCs/withLocationHandlers';
|
||||
import { createWebPlayer } from 'Player';
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
import withLocationHandlers from 'HOCs/withLocationHandlers';
|
||||
import { useStore } from 'App/mstore';
|
||||
import PlayerBlockHeader from './Player/ReplayPlayer/PlayerBlockHeader';
|
||||
import ReadNote from '../Session_/Player/Controls/components/ReadNote';
|
||||
import PlayerContent from './Player/ReplayPlayer/PlayerContent';
|
||||
import { IPlayerContext, PlayerContext, defaultContextValue } from './playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Note } from 'App/services/NotesService';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
|
||||
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Note } from 'App/services/NotesService';
|
||||
import { closeBottomBlock, toggleFullscreen } from 'Duck/components/player';
|
||||
import { fetchList } from 'Duck/integrations';
|
||||
import { Loader, Modal } from 'UI';
|
||||
|
||||
|
||||
|
||||
import ReadNote from '../Session_/Player/Controls/components/ReadNote';
|
||||
import PlayerBlockHeader from './Player/ReplayPlayer/PlayerBlockHeader';
|
||||
import PlayerContent from './Player/ReplayPlayer/PlayerContent';
|
||||
import { IPlayerContext, PlayerContext, defaultContextValue } from './playerContext';
|
||||
|
||||
|
||||
const TABS = {
|
||||
EVENTS: 'Activity',
|
||||
CLICKMAP: 'Click Map',
|
||||
|
|
@ -54,13 +61,21 @@ function WebPlayer(props: any) {
|
|||
useEffect(() => {
|
||||
playerInst = undefined;
|
||||
if (!session.sessionId || contextValue.player !== undefined) return;
|
||||
const mobData = sessionStore.prefetchedMobUrls[session.sessionId] as Record<string, any> | undefined;
|
||||
const usePrefetched = props.prefetched && mobData?.data;
|
||||
fetchList('issues');
|
||||
sessionStore.setUserTimezone(session.timezone);
|
||||
const [WebPlayerInst, PlayerStore] = createWebPlayer(
|
||||
session,
|
||||
(state) => makeAutoObservable(state),
|
||||
toast
|
||||
toast,
|
||||
props.prefetched,
|
||||
);
|
||||
if (usePrefetched) {
|
||||
if (mobData?.data) {
|
||||
WebPlayerInst.preloadFirstFile(mobData?.data)
|
||||
}
|
||||
}
|
||||
setContextValue({ player: WebPlayerInst, store: PlayerStore });
|
||||
playerInst = WebPlayerInst;
|
||||
|
||||
|
|
@ -78,6 +93,12 @@ function WebPlayer(props: any) {
|
|||
}
|
||||
}, [session.sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.prefetched && session.domURL.length > 0) {
|
||||
playerInst?.reinit(session)
|
||||
}
|
||||
}, [session.domURL.length, props.prefetched])
|
||||
|
||||
const { firstVisualEvent: visualOffset, messagesProcessed, tabStates, ready } = contextValue.store?.get() || {};
|
||||
const cssLoading = ready && tabStates ? Object.values(tabStates).some(
|
||||
({ cssLoading }) => cssLoading
|
||||
|
|
@ -205,6 +226,7 @@ export default connect(
|
|||
(state: any) => ({
|
||||
session: state.getIn(['sessions', 'current']),
|
||||
insights: state.getIn(['sessions', 'insights']),
|
||||
prefetched: state.getIn(['sessions', 'prefetched']),
|
||||
visitedEvents: state.getIn(['sessions', 'visitedEvents']),
|
||||
jwt: state.getIn(['user', 'jwt']),
|
||||
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, Icon } from 'UI';
|
||||
import { session as sessionRoute, liveSession as liveSessionRoute } from 'App/routes';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
import {
|
||||
liveSession as liveSessionRoute,
|
||||
session as sessionRoute,
|
||||
withSiteId,
|
||||
} from 'App/routes';
|
||||
import { Icon, Link } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const PLAY_ICON_NAMES = {
|
||||
notPlayed: 'play-fill',
|
||||
|
|
@ -8,7 +15,8 @@ const PLAY_ICON_NAMES = {
|
|||
hovered: 'play-hover',
|
||||
};
|
||||
|
||||
const getDefaultIconName = (isViewed: any) => (!isViewed ? PLAY_ICON_NAMES.notPlayed : PLAY_ICON_NAMES.played);
|
||||
const getDefaultIconName = (isViewed: any) =>
|
||||
!isViewed ? PLAY_ICON_NAMES.notPlayed : PLAY_ICON_NAMES.played;
|
||||
|
||||
interface Props {
|
||||
isAssist: boolean;
|
||||
|
|
@ -17,10 +25,13 @@ interface Props {
|
|||
onClick?: () => void;
|
||||
queryParams?: any;
|
||||
newTab?: boolean;
|
||||
query?: string
|
||||
query?: string;
|
||||
beforeOpen?: () => void;
|
||||
siteId?: string;
|
||||
}
|
||||
export default function PlayLink(props: Props) {
|
||||
function PlayLink(props: Props) {
|
||||
const { isAssist, viewed, sessionId, onClick = null, queryParams } = props;
|
||||
const history = useHistory();
|
||||
const defaultIconName = getDefaultIconName(viewed);
|
||||
|
||||
const [isHovered, toggleHover] = useState(false);
|
||||
|
|
@ -31,16 +42,33 @@ export default function PlayLink(props: Props) {
|
|||
else setIconName(getDefaultIconName(viewed));
|
||||
}, [isHovered, viewed]);
|
||||
|
||||
const link = isAssist ? liveSessionRoute(sessionId, queryParams) : sessionRoute(sessionId);
|
||||
const link = isAssist
|
||||
? liveSessionRoute(sessionId, queryParams)
|
||||
: sessionRoute(sessionId);
|
||||
|
||||
const handleBeforeOpen = () => {
|
||||
if (props.beforeOpen) {
|
||||
props.beforeOpen();
|
||||
history.push(withSiteId(link + (props.query ? props.query : ''), props.siteId));
|
||||
}
|
||||
};
|
||||
|
||||
const onLinkClick = props.beforeOpen ? handleBeforeOpen : onClick;
|
||||
return (
|
||||
<Link
|
||||
onClick={onClick ? onClick : () => {}}
|
||||
onClick={onLinkClick}
|
||||
to={link + (props.query ? props.query : '')}
|
||||
onMouseEnter={() => toggleHover(true)}
|
||||
onMouseLeave={() => toggleHover(false)}
|
||||
target={props.newTab ? "_blank" : undefined} rel={props.newTab ? "noopener noreferrer" : undefined}
|
||||
target={props.newTab ? '_blank' : undefined}
|
||||
rel={props.newTab ? 'noopener noreferrer' : undefined}
|
||||
>
|
||||
<Icon name={iconName} size={38} color={isAssist ? 'tealx' : 'teal'} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default connect((state: any, props: Props) => ({
|
||||
siteId: props.siteId || state.getIn([ 'site', 'siteId' ])
|
||||
}))(PlayLink);
|
||||
|
|
@ -1,26 +1,38 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { CountryFlag, Avatar, TextEllipsis, Label, Icon, Tooltip, ItemMenu } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { Duration } from 'luxon';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useMemo } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { durationFormatted, formatTimeOrDate } from 'App/date';
|
||||
import stl from './sessionItem.module.css';
|
||||
import Counter from './Counter';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import SessionMetaList from './SessionMetaList';
|
||||
import PlayLink from './PlayLink';
|
||||
import ErrorBars from './ErrorBars';
|
||||
import { presetSession } from 'App/duck/sessions';
|
||||
import { useStore } from 'App/mstore';
|
||||
import {
|
||||
assist as assistRoute,
|
||||
isRoute,
|
||||
liveSession,
|
||||
sessions as sessionsRoute,
|
||||
session as sessionRoute,
|
||||
isRoute
|
||||
sessions as sessionsRoute,
|
||||
} from 'App/routes';
|
||||
import { capitalize } from 'App/utils';
|
||||
import { Duration } from 'luxon';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
Avatar,
|
||||
CountryFlag,
|
||||
Icon,
|
||||
ItemMenu,
|
||||
Label,
|
||||
TextEllipsis,
|
||||
Tooltip,
|
||||
} from 'UI';
|
||||
|
||||
import Counter from './Counter';
|
||||
import ErrorBars from './ErrorBars';
|
||||
import PlayLink from './PlayLink';
|
||||
import SessionMetaList from './SessionMetaList';
|
||||
import stl from './sessionItem.module.css';
|
||||
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
const ASSIST_LIVE_SESSION = liveSession();
|
||||
|
|
@ -69,12 +81,20 @@ interface Props {
|
|||
ignoreAssist?: boolean;
|
||||
bookmarked?: boolean;
|
||||
toggleFavorite?: (sessionId: string) => void;
|
||||
query?: string
|
||||
query?: string;
|
||||
presetSession?: typeof presetSession;
|
||||
}
|
||||
|
||||
const PREFETCH_STATE = {
|
||||
none: 0,
|
||||
loading: 1,
|
||||
fetched: 2,
|
||||
}
|
||||
|
||||
function SessionItem(props: RouteComponentProps & Props) {
|
||||
const { settingsStore } = useStore();
|
||||
const { settingsStore, sessionStore } = useStore();
|
||||
const { timezone, shownTimezone } = settingsStore.sessionSettings;
|
||||
const [prefetchState, setPrefetched] = React.useState(PREFETCH_STATE.none);
|
||||
|
||||
const {
|
||||
session,
|
||||
|
|
@ -88,6 +108,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
ignoreAssist = false,
|
||||
bookmarked = false,
|
||||
query,
|
||||
presetSession,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
|
|
@ -110,7 +131,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
metadata,
|
||||
issueTypes,
|
||||
active,
|
||||
timezone: userTimezone
|
||||
timezone: userTimezone,
|
||||
} = session;
|
||||
|
||||
const location = props.location;
|
||||
|
|
@ -120,11 +141,11 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
const hasUserId = userId || userAnonymousId;
|
||||
const isSessions = isRoute(SESSIONS_ROUTE, location.pathname);
|
||||
const isAssist =
|
||||
!ignoreAssist &&
|
||||
(!ignoreAssist &&
|
||||
(isRoute(ASSIST_ROUTE, location.pathname) ||
|
||||
isRoute(ASSIST_LIVE_SESSION, location.pathname) ||
|
||||
location.pathname.includes('multiview')) ||
|
||||
props.live
|
||||
location.pathname.includes('multiview'))) ||
|
||||
props.live;
|
||||
|
||||
const isLastPlayed = lastPlayedSessionId === sessionId;
|
||||
|
||||
|
|
@ -146,16 +167,32 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
}${sessionRoute(sessionId)}`;
|
||||
copy(sessionPath);
|
||||
toast.success('Session URL copied to clipboard');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'trash',
|
||||
text: 'Remove',
|
||||
onClick: () => (props.toggleFavorite ? props.toggleFavorite(sessionId) : null)
|
||||
}
|
||||
onClick: () =>
|
||||
props.toggleFavorite ? props.toggleFavorite(sessionId) : null,
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
||||
const handleHover = async () => {
|
||||
if (prefetchState !== PREFETCH_STATE.none || props.live || isAssist) return;
|
||||
|
||||
setPrefetched(PREFETCH_STATE.loading);
|
||||
try {
|
||||
await sessionStore.getFirstMob(sessionId);
|
||||
setPrefetched(PREFETCH_STATE.fetched);
|
||||
} catch (e) {
|
||||
console.error('Error while prefetching first mob', e);
|
||||
}
|
||||
};
|
||||
const openSession = () => {
|
||||
if (props.live || isAssist || prefetchState === PREFETCH_STATE.none) return
|
||||
presetSession?.(session);
|
||||
};
|
||||
return (
|
||||
<Tooltip
|
||||
delay={0}
|
||||
|
|
@ -164,23 +201,31 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
>
|
||||
<div
|
||||
className={cn(stl.sessionItem, 'flex flex-col py-2 px-4')}
|
||||
id='session-item'
|
||||
id="session-item"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className='flex items-start'>
|
||||
<div className="flex items-start">
|
||||
<div className={cn('flex items-center w-full')}>
|
||||
{!compact && (
|
||||
<div className='flex items-center pr-2 shrink-0' style={{ width: '40%' }}>
|
||||
<div>
|
||||
<Avatar isActive={active} seed={userNumericHash} isAssist={isAssist} />
|
||||
</div>
|
||||
<div
|
||||
className='flex flex-col overflow-hidden color-gray-medium ml-3 justify-between items-center shrink-0'>
|
||||
className="flex items-center pr-2 shrink-0"
|
||||
style={{ width: '40%' }}
|
||||
>
|
||||
<div>
|
||||
<Avatar
|
||||
isActive={active}
|
||||
seed={userNumericHash}
|
||||
isAssist={isAssist}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col overflow-hidden color-gray-medium ml-3 justify-between items-center shrink-0">
|
||||
<div
|
||||
className={cn('text-lg', {
|
||||
'color-teal cursor-pointer': !disableUser && hasUserId && !props.isDisabled,
|
||||
[stl.userName]: !disableUser && hasUserId && !props.isDisabled,
|
||||
'color-gray-medium': disableUser || !hasUserId
|
||||
'color-teal cursor-pointer':
|
||||
!disableUser && hasUserId && !props.isDisabled,
|
||||
[stl.userName]:
|
||||
!disableUser && hasUserId && !props.isDisabled,
|
||||
'color-gray-medium': disableUser || !hasUserId,
|
||||
})}
|
||||
onClick={() =>
|
||||
!disableUser && !hasUserFilter && hasUserId
|
||||
|
|
@ -190,7 +235,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
>
|
||||
<TextEllipsis
|
||||
text={userDisplayName}
|
||||
maxWidth='200px'
|
||||
maxWidth="200px"
|
||||
popupProps={{ inverted: true, size: 'tiny' }}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -199,7 +244,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
)}
|
||||
<div
|
||||
style={{ width: compact ? '40%' : '20%' }}
|
||||
className='px-2 flex flex-col justify-between'
|
||||
className="px-2 flex flex-col justify-between"
|
||||
>
|
||||
<div>
|
||||
<Tooltip
|
||||
|
|
@ -208,7 +253,9 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
title={
|
||||
<div className={'flex flex-col gap-1'}>
|
||||
<span>
|
||||
Local Time: {formatTimeOrDate(startedAt, timezone, true)} {timezone.label}
|
||||
Local Time:{' '}
|
||||
{formatTimeOrDate(startedAt, timezone, true)}{' '}
|
||||
{timezone.label}
|
||||
</span>
|
||||
{userTimezone ? (
|
||||
<span>
|
||||
|
|
@ -217,7 +264,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
startedAt,
|
||||
{
|
||||
label: userTimezone.split('+').join(' +'),
|
||||
value: userTimezone.split(':')[0]
|
||||
value: userTimezone.split(':')[0],
|
||||
},
|
||||
true
|
||||
)}{' '}
|
||||
|
|
@ -226,7 +273,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
) : null}
|
||||
</div>
|
||||
}
|
||||
className='w-fit !block'
|
||||
className="w-fit !block"
|
||||
>
|
||||
<TextEllipsis
|
||||
text={formatTimeOrDate(
|
||||
|
|
@ -234,7 +281,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
shownTimezone === 'user' && userTimezone
|
||||
? {
|
||||
label: userTimezone.split('+').join(' +'),
|
||||
value: userTimezone.split(':')[0]
|
||||
value: userTimezone.split(':')[0],
|
||||
}
|
||||
: timezone
|
||||
)}
|
||||
|
|
@ -242,20 +289,33 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='flex items-center color-gray-medium py-1'>
|
||||
<div className="flex items-center color-gray-medium py-1">
|
||||
{!isAssist && (
|
||||
<>
|
||||
<div className='color-gray-medium'>
|
||||
<span className='mr-1'>{eventsCount}</span>
|
||||
<span>{eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event'}</span>
|
||||
<div className="color-gray-medium">
|
||||
<span className="mr-1">{eventsCount}</span>
|
||||
<span>
|
||||
{eventsCount === 0 || eventsCount > 1
|
||||
? 'Events'
|
||||
: 'Event'}
|
||||
</span>
|
||||
</div>
|
||||
<Icon name='circle-fill' size={3} className='mx-4' />
|
||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||
</>
|
||||
)}
|
||||
<div>{live || props.live ? <Counter startTime={startedAt} /> : formattedDuration}</div>
|
||||
<div>
|
||||
{live || props.live ? (
|
||||
<Counter startTime={startedAt} />
|
||||
) : (
|
||||
formattedDuration
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: '30%' }} className='px-2 flex flex-col justify-between'>
|
||||
</div>
|
||||
<div
|
||||
style={{ width: '30%' }}
|
||||
className="px-2 flex flex-col justify-between"
|
||||
>
|
||||
<div style={{ height: '21px' }}>
|
||||
<CountryFlag
|
||||
userCity={userCity}
|
||||
|
|
@ -276,14 +336,17 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
{userOs && userBrowser ? (
|
||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||
) : null}
|
||||
<span className={/ios/i.test(userOs) ? '' : 'capitalize'} style={{ maxWidth: '70px' }}>
|
||||
<span
|
||||
className={/ios/i.test(userOs) ? '' : 'capitalize'}
|
||||
style={{ maxWidth: '70px' }}
|
||||
>
|
||||
<TextEllipsis
|
||||
text={/ios/i.test(userOs) ? 'iOS' : capitalize(userOs)}
|
||||
popupProps={{ inverted: true, size: 'tiny' }}
|
||||
/>
|
||||
</span>
|
||||
<Icon name='circle-fill' size={3} className='mx-4' />
|
||||
<span className='capitalize' style={{ maxWidth: '70px' }}>
|
||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||
<span className="capitalize" style={{ maxWidth: '70px' }}>
|
||||
<TextEllipsis
|
||||
text={capitalize(userDeviceType)}
|
||||
popupProps={{ inverted: true, size: 'tiny' }}
|
||||
|
|
@ -292,35 +355,44 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
</div>
|
||||
</div>
|
||||
{isSessions && (
|
||||
<div style={{ width: '10%' }} className='self-center px-2 flex items-center'>
|
||||
<div
|
||||
style={{ width: '10%' }}
|
||||
className="self-center px-2 flex items-center"
|
||||
>
|
||||
<ErrorBars count={issueTypes.length} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex items-center'>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={cn(
|
||||
stl.playLink,
|
||||
props.isDisabled ? 'cursor-not-allowed' : 'cursor-pointer'
|
||||
)}
|
||||
id='play-button'
|
||||
id="play-button"
|
||||
data-viewed={viewed}
|
||||
>
|
||||
{live && session.isCallActive && session.agentIds!.length > 0 ? (
|
||||
<div className='mr-4'>
|
||||
<Label className='bg-gray-lightest p-1 px-2 rounded-lg'>
|
||||
<span className='color-gray-medium text-xs' style={{ whiteSpace: 'nowrap' }}>
|
||||
<div className="mr-4">
|
||||
<Label className="bg-gray-lightest p-1 px-2 rounded-lg">
|
||||
<span
|
||||
className="color-gray-medium text-xs"
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
CALL IN PROGRESS
|
||||
</span>
|
||||
</Label>
|
||||
</div>
|
||||
) : null}
|
||||
{isSessions && (
|
||||
<div className='mr-4 flex-shrink-0 w-24'>
|
||||
<div className="mr-4 flex-shrink-0 w-24">
|
||||
{isLastPlayed && (
|
||||
<Label className='bg-gray-lightest p-1 px-2 rounded-lg'>
|
||||
<span className='color-gray-medium text-xs' style={{ whiteSpace: 'nowrap' }}>
|
||||
<Label className="bg-gray-lightest p-1 px-2 rounded-lg">
|
||||
<span
|
||||
className="color-gray-medium text-xs"
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
LAST PLAYED
|
||||
</span>
|
||||
</Label>
|
||||
|
|
@ -329,15 +401,15 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
)}
|
||||
{props.isAdd ? (
|
||||
<div
|
||||
className='rounded-full border-tealx p-2 border'
|
||||
className="rounded-full border-tealx p-2 border"
|
||||
onClick={() => (props.isDisabled ? null : props.onClick())}
|
||||
>
|
||||
<div className='bg-tealx rounded-full p-2'>
|
||||
<Icon name='plus' size={16} color='white' />
|
||||
<div className="bg-tealx rounded-full p-2">
|
||||
<Icon name="plus" size={16} color="white" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div onMouseEnter={handleHover}>
|
||||
<PlayLink
|
||||
isAssist={isAssist}
|
||||
sessionId={sessionId}
|
||||
|
|
@ -345,21 +417,24 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
onClick={onClick}
|
||||
queryParams={queryParams}
|
||||
query={query}
|
||||
beforeOpen={props.live || isAssist ? undefined : openSession}
|
||||
/>
|
||||
{bookmarked && (
|
||||
<div className='ml-2 cursor-pointer'>
|
||||
<div className="ml-2 cursor-pointer">
|
||||
<ItemMenu bold items={menuItems} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{_metaList.length > 0 && <SessionMetaList className='mt-4' metaList={_metaList} />}
|
||||
{_metaList.length > 0 && (
|
||||
<SessionMetaList className="mt-4" metaList={_metaList} />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(observer(SessionItem));
|
||||
export default withRouter(connect(null, { presetSession })(observer(SessionItem)));
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ const SET_ACTIVE_TAB = 'sessions/SET_ACTIVE_TAB';
|
|||
|
||||
const CLEAR_CURRENT_SESSION = 'sessions/CLEAR_CURRENT_SESSION'
|
||||
|
||||
const PREFETCH_SESSION = 'sessions/PREFETCH_SESSION'
|
||||
|
||||
const range = getDateRangeFromValue(LAST_7_DAYS);
|
||||
const defaultDateFilters = {
|
||||
url: '',
|
||||
|
|
@ -60,6 +62,7 @@ const initObj = {
|
|||
list: [],
|
||||
sessionIds: [],
|
||||
current: new Session(),
|
||||
prefetched: false,
|
||||
eventsAsked: false,
|
||||
total: 0,
|
||||
keyMap: Map(),
|
||||
|
|
@ -185,6 +188,13 @@ const reducer = (state = initialState, action: IAction) => {
|
|||
|
||||
return state
|
||||
.set('current', session)
|
||||
.set('prefetched', false)
|
||||
}
|
||||
case PREFETCH_SESSION: {
|
||||
const { data } = action;
|
||||
return state
|
||||
.set('current', data)
|
||||
.set('prefetched', true);
|
||||
}
|
||||
case FETCH_EVENTS.SUCCESS: {
|
||||
const {
|
||||
|
|
@ -454,6 +464,13 @@ export const fetchV2 = (sessionId: string) =>
|
|||
|
||||
}
|
||||
|
||||
export function presetSession(sessionData) {
|
||||
return {
|
||||
type: PREFETCH_SESSION,
|
||||
data: sessionData
|
||||
}
|
||||
}
|
||||
|
||||
export function clearCurrentSession() {
|
||||
return {
|
||||
type: CLEAR_CURRENT_SESSION
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { getDateRangeFromValue } from "App/dateRange";
|
|||
import { getRE, setSessionFilter, getSessionFilter, compareJsonObjects, cleanSessionFilters } from 'App/utils';
|
||||
import store from 'App/store'
|
||||
import { Note } from "App/services/NotesService";
|
||||
import { loadFile } from "../player/web/network/loadFiles";
|
||||
|
||||
class UserFilter {
|
||||
endDate: number = new Date().getTime();
|
||||
|
|
@ -125,6 +126,7 @@ export default class SessionStore {
|
|||
previousId = ''
|
||||
nextId = ''
|
||||
userTimezone = ''
|
||||
prefetchedMobUrls: Record<string, { data: Uint8Array, entryNum: number }> = {}
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
|
|
@ -141,6 +143,28 @@ export default class SessionStore {
|
|||
this.userFilter = new UserFilter();
|
||||
}
|
||||
|
||||
async getFirstMob(sessionId: string) {
|
||||
const { domURL } = await sessionService.getFirstMobUrl(sessionId)
|
||||
await loadFile(
|
||||
domURL[0],
|
||||
(data) => this.setPrefetchedMobUrl(sessionId, data)
|
||||
)
|
||||
}
|
||||
|
||||
setPrefetchedMobUrl(sessionId: string, fileData: Uint8Array) {
|
||||
const keys = Object.keys(this.prefetchedMobUrls)
|
||||
const toLimit = 10 - keys.length
|
||||
if (toLimit < 0) {
|
||||
const oldest = keys.sort(
|
||||
(a, b) => this.prefetchedMobUrls[a].entryNum - this.prefetchedMobUrls[b].entryNum
|
||||
)[0]
|
||||
delete this.prefetchedMobUrls[oldest]
|
||||
}
|
||||
const nextEntryNum = keys.length > 0
|
||||
? Math.max(...keys.map(key => this.prefetchedMobUrls[key].entryNum)) + 1 : 0
|
||||
this.prefetchedMobUrls[sessionId] = { data: fileData, entryNum: nextEntryNum }
|
||||
}
|
||||
|
||||
getSessions(filter: any): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
sessionService
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ export function createIOSPlayer(
|
|||
export function createWebPlayer(
|
||||
session: SessionFilesInfo,
|
||||
wrapStore?: (s: IWebPlayerStore) => IWebPlayerStore,
|
||||
uiErrorHandler?: { error: (msg: string) => void }
|
||||
uiErrorHandler?: { error: (msg: string) => void },
|
||||
prefetched?: boolean,
|
||||
): [IWebPlayer, IWebPlayerStore] {
|
||||
let store: WebPlayerStore = new SimpleStore<WebState>({
|
||||
...WebPlayer.INITIAL_STATE,
|
||||
|
|
@ -49,7 +50,7 @@ export function createWebPlayer(
|
|||
store = wrapStore(store);
|
||||
}
|
||||
|
||||
const player = new WebPlayer(store, session, false, false, uiErrorHandler);
|
||||
const player = new WebPlayer(store, session, false, false, uiErrorHandler, prefetched);
|
||||
return [player, store];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,13 +32,17 @@ export default class MessageLoader {
|
|||
};
|
||||
|
||||
constructor(
|
||||
private readonly session: SessionFilesInfo,
|
||||
private session: SessionFilesInfo,
|
||||
private store: Store<State>,
|
||||
private messageManager: MessageManager | IOSMessageManager,
|
||||
private isClickmap: boolean,
|
||||
private uiErrorHandler?: { error: (msg: string) => void }
|
||||
) {}
|
||||
|
||||
setSession(session: SessionFilesInfo) {
|
||||
this.session = session
|
||||
}
|
||||
|
||||
createNewParser(
|
||||
shouldDecrypt = true,
|
||||
onMessagesDone: (msgs: PlayerMsg[], file?: string) => void,
|
||||
|
|
@ -145,6 +149,18 @@ export default class MessageLoader {
|
|||
}
|
||||
}
|
||||
|
||||
preloaded = false;
|
||||
async preloadFirstFile(data: Uint8Array) {
|
||||
this.mobParser = this.createNewParser(true, this.processMessages, 'p:dom');
|
||||
|
||||
try {
|
||||
await this.mobParser(data)
|
||||
this.preloaded = true;
|
||||
} catch (e) {
|
||||
console.error('error parsing msgs', e)
|
||||
}
|
||||
}
|
||||
|
||||
async loadDomFiles(urls: string[], parser: (b: Uint8Array) => Promise<void>) {
|
||||
if (urls.length > 0) {
|
||||
this.store.update({ domLoading: true });
|
||||
|
|
@ -197,21 +213,25 @@ export default class MessageLoader {
|
|||
}
|
||||
}
|
||||
|
||||
mobParser: (b: Uint8Array) => Promise<void>
|
||||
loadMobs = async () => {
|
||||
const loadMethod =
|
||||
this.session.domURL && this.session.domURL.length > 0
|
||||
? {
|
||||
mobUrls: this.session.domURL,
|
||||
parser: () =>
|
||||
this.createNewParser(true, this.processMessages, 'dom'),
|
||||
this.createNewParser(true, this.processMessages, 'd:dom'),
|
||||
}
|
||||
: {
|
||||
mobUrls: this.session.mobsUrl,
|
||||
parser: () =>
|
||||
this.createNewParser(false, this.processMessages, 'dom'),
|
||||
this.createNewParser(false, this.processMessages, 'm:dom'),
|
||||
};
|
||||
|
||||
const parser = loadMethod.parser();
|
||||
if (!this.mobParser) {
|
||||
this.mobParser = loadMethod.parser();
|
||||
}
|
||||
const parser = this.mobParser
|
||||
const devtoolsParser = this.createNewParser(
|
||||
true,
|
||||
this.processMessages,
|
||||
|
|
@ -225,7 +245,7 @@ export default class MessageLoader {
|
|||
* as a tradeoff we have some copy-paste code
|
||||
* for the devtools file
|
||||
* */
|
||||
await loadFiles([loadMethod.mobUrls[0]], parser);
|
||||
if (!this.preloaded) await loadFiles([loadMethod.mobUrls[0]], parser);
|
||||
this.messageManager.onFileReadFinally();
|
||||
const restDomFilesPromise = this.loadDomFiles(
|
||||
[...loadMethod.mobUrls.slice(1)],
|
||||
|
|
|
|||
|
|
@ -237,9 +237,9 @@ export default class MessageManager {
|
|||
});
|
||||
|
||||
if (
|
||||
this.waitingForFiles &&
|
||||
this.lastMessageTime <= t &&
|
||||
t !== this.session.duration.milliseconds
|
||||
this.waitingForFiles ||
|
||||
(this.lastMessageTime <= t &&
|
||||
t !== this.session.duration.milliseconds)
|
||||
) {
|
||||
this.setMessagesLoading(true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,10 @@ export default class WebPlayer extends Player {
|
|||
|
||||
liveTimeTravel: false,
|
||||
inspectorMode: false,
|
||||
mobsFetched: false,
|
||||
}
|
||||
|
||||
private readonly inspectorController: InspectorController
|
||||
private inspectorController: InspectorController
|
||||
protected screen: Screen
|
||||
protected readonly messageManager: MessageManager
|
||||
protected readonly messageLoader: MessageLoader
|
||||
|
|
@ -34,7 +35,8 @@ export default class WebPlayer extends Player {
|
|||
session: SessionFilesInfo,
|
||||
live: boolean,
|
||||
isClickMap = false,
|
||||
public readonly uiErrorHandler?: { error: (msg: string) => void }
|
||||
public readonly uiErrorHandler?: { error: (msg: string) => void },
|
||||
private readonly prefetched?: boolean,
|
||||
) {
|
||||
let initialLists = live ? {} : {
|
||||
event: session.events || [],
|
||||
|
|
@ -62,8 +64,10 @@ export default class WebPlayer extends Player {
|
|||
this.screen = screen
|
||||
this.messageManager = messageManager
|
||||
this.messageLoader = messageLoader
|
||||
if (!live) { // hack. TODO: split OfflinePlayer class
|
||||
|
||||
if (!live && !prefetched) { // hack. TODO: split OfflinePlayer class
|
||||
void messageLoader.loadFiles()
|
||||
wpState.update({ mobsFetched: true })
|
||||
}
|
||||
|
||||
this.targetMarker = new TargetMarker(this.screen, wpState)
|
||||
|
|
@ -84,6 +88,30 @@ export default class WebPlayer extends Player {
|
|||
window.playerJumpToTime = this.jump.bind(this)
|
||||
}
|
||||
|
||||
preloadFirstFile(data: Uint8Array) {
|
||||
void this.messageLoader.preloadFirstFile(data)
|
||||
}
|
||||
|
||||
reinit(session: SessionFilesInfo) {
|
||||
if (this.wpState.get().mobsFetched) return; // already initialized
|
||||
this.messageLoader.setSession(session)
|
||||
void this.messageLoader.loadFiles();
|
||||
|
||||
this.targetMarker = new TargetMarker(this.screen, this.wpState)
|
||||
this.inspectorController = new InspectorController(this.screen, this.wpState)
|
||||
|
||||
|
||||
const endTime = session.duration?.valueOf() || 0
|
||||
this.wpState.update({
|
||||
//@ts-ignore
|
||||
session,
|
||||
endTime, // : 0,
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
window.playerJumpToTime = this.jump.bind(this)
|
||||
}
|
||||
|
||||
updateLists = (session: any) => {
|
||||
const lists = {
|
||||
event: session.events || [],
|
||||
|
|
|
|||
|
|
@ -46,6 +46,14 @@ export default class SettingsService {
|
|||
.catch((e) => Promise.reject(e));
|
||||
}
|
||||
|
||||
getFirstMobUrl(sessionId: string): Promise<{ domURL: string[] }> {
|
||||
return this.client
|
||||
.get(`/sessions/${sessionId}/first-mob`)
|
||||
.then((r) => r.json())
|
||||
.then((j) => j.data || {})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
getSessionInfo(sessionId: string, isLive?: boolean): Promise<ISession> {
|
||||
return this.client
|
||||
.get(isLive ? `/assist/sessions/${sessionId}` : `/sessions/${sessionId}`)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue