diff --git a/frontend/app/components/Session/Session.tsx b/frontend/app/components/Session/Session.tsx index ec4f969be..ee115db04 100644 --- a/frontend/app/components/Session/Session.tsx +++ b/frontend/app/components/Session/Session.tsx @@ -1,86 +1,99 @@ +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(); interface Props { - sessionId: string; - loading: boolean; - hasErrors: boolean; - fetchV2: (sessionId: string) => void; - clearCurrentSession: () => void; - session: Record; + sessionId: string; + loading: boolean; + hasErrors: boolean; + fetchV2: (sessionId: string) => void; + clearCurrentSession: () => void; + session: Record; } -function Session({ - sessionId, - loading, - hasErrors, - fetchV2, - clearCurrentSession, - session, - }: Props) { - usePageTitle("OpenReplay Session Player"); - const [ initializing, setInitializing ] = useState(true) - const { sessionStore } = useStore(); - useEffect(() => { - if (sessionId != null) { - fetchV2(sessionId) - } else { - console.error("No sessionID in route.") - } - setInitializing(false) - return () => { - clearCurrentSession(); - } - },[ sessionId ]); +function Session({ + sessionId, + hasErrors, + fetchV2, + clearCurrentSession, + session, +}: Props) { + usePageTitle('OpenReplay Session Player'); + const { sessionStore } = useStore(); + useEffect(() => { + if (sessionId != null) { + fetchV2(sessionId); + } else { + console.error('No sessionID in route.'); + } + return () => { + clearCurrentSession(); + }; + }, [sessionId]); - useEffect(() => { - clearLogs() - sessionStore.resetUserFilter(); - } ,[]) - - const player = session.isMobileNative ? : - return ( - - {'Please check your data retention plan, or try '} - {'another one'} - - } - > - - {player} - - - ); + useEffect(() => { + clearLogs(); + sessionStore.resetUserFilter(); + }, []); + + const player = session.isMobileNative ? : ; + return ( + + {'Please check your data retention plan, or try '} + + {'another one'} + + + } + > + + {player} + + + ); } -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' ]), - }; - }, { - fetchSlackList, - fetchV2, - clearCurrentSession, -})(Session)); +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']), + }; + }, + { + fetchSlackList, + fetchV2, + clearCurrentSession, + } + )(Session) +); diff --git a/frontend/app/components/Session/WebPlayer.tsx b/frontend/app/components/Session/WebPlayer.tsx index 72ab5bfcb..fd1e250d9 100644 --- a/frontend/app/components/Session/WebPlayer.tsx +++ b/frontend/app/components/Session/WebPlayer.tsx @@ -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 | 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']), diff --git a/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx b/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx index 751a2a178..459b3a7da 100644 --- a/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx +++ b/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx @@ -1,46 +1,74 @@ -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', - played: 'play-circle-light', - hovered: 'play-hover', + notPlayed: 'play-fill', + played: 'play-circle-light', + 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; - viewed: boolean; - sessionId: string; - onClick?: () => void; - queryParams?: any; - newTab?: boolean; - query?: string + isAssist: boolean; + viewed: boolean; + sessionId: string; + onClick?: () => void; + queryParams?: any; + newTab?: boolean; + query?: string; + beforeOpen?: () => void; + siteId?: string; } -export default function PlayLink(props: Props) { - const { isAssist, viewed, sessionId, onClick = null, queryParams } = props; - const defaultIconName = getDefaultIconName(viewed); +function PlayLink(props: Props) { + const { isAssist, viewed, sessionId, onClick = null, queryParams } = props; + const history = useHistory(); + const defaultIconName = getDefaultIconName(viewed); - const [isHovered, toggleHover] = useState(false); - const [iconName, setIconName] = useState(defaultIconName); + const [isHovered, toggleHover] = useState(false); + const [iconName, setIconName] = useState(defaultIconName); - useEffect(() => { - if (isHovered) setIconName(PLAY_ICON_NAMES.hovered); - else setIconName(getDefaultIconName(viewed)); - }, [isHovered, viewed]); + useEffect(() => { + if (isHovered) setIconName(PLAY_ICON_NAMES.hovered); + else setIconName(getDefaultIconName(viewed)); + }, [isHovered, viewed]); - const link = isAssist ? liveSessionRoute(sessionId, queryParams) : sessionRoute(sessionId); - return ( - {}} - to={link + (props.query ? props.query : '')} - onMouseEnter={() => toggleHover(true)} - onMouseLeave={() => toggleHover(false)} - target={props.newTab ? "_blank" : undefined} rel={props.newTab ? "noopener noreferrer" : undefined} - > - - - ); + 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 ( + toggleHover(true)} + onMouseLeave={() => toggleHover(false)} + target={props.newTab ? '_blank' : undefined} + rel={props.newTab ? 'noopener noreferrer' : undefined} + > + + + ); } + + +export default connect((state: any, props: Props) => ({ + siteId: props.siteId || state.getIn([ 'site', 'siteId' ]) +}))(PlayLink); \ No newline at end of file diff --git a/frontend/app/components/shared/SessionItem/SessionItem.tsx b/frontend/app/components/shared/SessionItem/SessionItem.tsx index a8d794979..2bb233f24 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.tsx +++ b/frontend/app/components/shared/SessionItem/SessionItem.tsx @@ -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 && - (isRoute(ASSIST_ROUTE, location.pathname) || - isRoute(ASSIST_LIVE_SESSION, location.pathname) || - location.pathname.includes('multiview')) || - props.live + (!ignoreAssist && + (isRoute(ASSIST_ROUTE, location.pathname) || + isRoute(ASSIST_LIVE_SESSION, location.pathname) || + 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 (
e.stopPropagation()} > -
+
{!compact && ( -
+
- +
-
+
!disableUser && !hasUserFilter && hasUserId @@ -190,7 +235,7 @@ function SessionItem(props: RouteComponentProps & Props) { >
@@ -199,7 +244,7 @@ function SessionItem(props: RouteComponentProps & Props) { )}
- Local Time: {formatTimeOrDate(startedAt, timezone, true)} {timezone.label} + Local Time:{' '} + {formatTimeOrDate(startedAt, timezone, true)}{' '} + {timezone.label} {userTimezone ? ( @@ -217,7 +264,7 @@ function SessionItem(props: RouteComponentProps & Props) { startedAt, { label: userTimezone.split('+').join(' +'), - value: userTimezone.split(':')[0] + value: userTimezone.split(':')[0], }, true )}{' '} @@ -226,36 +273,49 @@ function SessionItem(props: RouteComponentProps & Props) { ) : null}
} - className='w-fit !block' + className="w-fit !block" >
-
+
{!isAssist && ( <> -
- {eventsCount} - {eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event'} +
+ {eventsCount} + + {eventsCount === 0 || eventsCount > 1 + ? 'Events' + : 'Event'} +
- + )} -
{live || props.live ? : formattedDuration}
+
+ {live || props.live ? ( + + ) : ( + formattedDuration + )} +
-
+
) : null} - + - - + +
{isSessions && ( -
+
)}
-
+
{live && session.isCallActive && session.agentIds!.length > 0 ? ( -
-