diff --git a/frontend/app/Router.js b/frontend/app/Router.js index 0c0e7433a..86f07c4f6 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -11,6 +11,7 @@ import UpdatePassword from 'Components/UpdatePassword/UpdatePassword'; import ClientPure from 'Components/Client/Client'; import OnboardingPure from 'Components/Onboarding/Onboarding'; import SessionPure from 'Components/Session/Session'; +import LiveSessionPure from 'Components/Session/LiveSession'; import AssistPure from 'Components/Assist'; import BugFinderPure from 'Components/BugFinder/BugFinder'; import DashboardPure from 'Components/Dashboard/Dashboard'; @@ -31,6 +32,7 @@ import { setSessionPath } from 'Duck/sessions'; const BugFinder = withSiteIdUpdater(BugFinderPure); const Dashboard = withSiteIdUpdater(DashboardPure); const Session = withSiteIdUpdater(SessionPure); +const LiveSession = withSiteIdUpdater(LiveSessionPure); const Assist = withSiteIdUpdater(AssistPure); const Client = withSiteIdUpdater(ClientPure); const Onboarding = withSiteIdUpdater(OnboardingPure); @@ -119,7 +121,7 @@ class Router extends React.Component { render() { const { isLoggedIn, jwt, siteId, sites, loading, changePassword, location, existingTenant, onboarding } = this.props; const siteIdList = sites.map(({ id }) => id).toJS(); - const hideHeader = location.pathname && location.pathname.includes('/session/'); + const hideHeader = location.pathname && location.pathname.includes('/session/') || location.pathname.includes('/assist/'); return isLoggedIn ? @@ -160,6 +162,7 @@ class Router extends React.Component { + } /> { routes.redirects.map(([ fr, to ]) => ( diff --git a/frontend/app/components/Assist/Assist.tsx b/frontend/app/components/Assist/Assist.tsx index 77730f7b1..0d901337b 100644 --- a/frontend/app/components/Assist/Assist.tsx +++ b/frontend/app/components/Assist/Assist.tsx @@ -5,13 +5,10 @@ import cn from 'classnames' import withPageTitle from 'HOCs/withPageTitle'; import withPermissions from 'HOCs/withPermissions' -// @withPageTitle("Assist - OpenReplay") function Assist() { return (
- {/*
-
*/}
@@ -22,4 +19,4 @@ function Assist() { ) } -export default withPageTitle("Assist - OpenReplay")(withPermissions(['ASSIST_LIVE', 'SESSION_REPLAY'])(Assist)); +export default withPageTitle("Assist - OpenReplay")(withPermissions(['ASSIST_LIVE'])(Assist)); diff --git a/frontend/app/components/Session/LivePlayer.js b/frontend/app/components/Session/LivePlayer.js index 81b4df5e0..967807556 100644 --- a/frontend/app/components/Session/LivePlayer.js +++ b/frontend/app/components/Session/LivePlayer.js @@ -12,27 +12,19 @@ import { import withPermissions from 'HOCs/withPermissions'; import PlayerBlockHeader from '../Session_/PlayerBlockHeader'; -import EventsBlock from '../Session_/EventsBlock'; import PlayerBlock from '../Session_/PlayerBlock'; import styles from '../Session_/session.css'; - -const EventsBlockConnected = connectPlayer(state => ({ - currentTimeEventIndex: state.eventListNow.length > 0 ? state.eventListNow.length - 1 : 0, - playing: state.playing, -}))(EventsBlock) - - const InitLoader = connectPlayer(state => ({ loading: !state.initialized }))(Loader); -function WebPlayer ({ showAssist, session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt, loadingCredentials, assistCredendials, request, isEnterprise, hasSessionsPath }) { +function LivePlayer ({ session, toggleFullscreen, closeBottomBlock, fullscreen, jwt, loadingCredentials, assistCredendials, request, isEnterprise, hasErrors }) { useEffect(() => { if (!loadingCredentials) { - initPlayer(session, jwt, assistCredendials, !hasSessionsPath && session.live); + initPlayer(session, jwt, assistCredendials, true); } return () => cleanPlayer() }, [ session.sessionId, loadingCredentials, assistCredendials ]); @@ -48,11 +40,10 @@ function WebPlayer ({ showAssist, session, toggleFullscreen, closeBottomBlock, l } }, []) - return ( - +
@@ -78,7 +69,8 @@ export default withRequest({ fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]), hasSessionsPath: hasSessioPath && !isAssist, isEnterprise: state.getIn([ 'user', 'client', 'edition' ]) === 'ee', + hasErrors: !!state.getIn([ 'sessions', 'errors' ]), } }, { toggleFullscreen, closeBottomBlock }, -)(WebPlayer))); +)(LivePlayer))); diff --git a/frontend/app/components/Session/LiveSession.js b/frontend/app/components/Session/LiveSession.js new file mode 100644 index 000000000..46bd39cf5 --- /dev/null +++ b/frontend/app/components/Session/LiveSession.js @@ -0,0 +1,60 @@ +import { useEffect } from 'react'; +import { connect } from 'react-redux'; +import usePageTitle from 'App/hooks/usePageTitle'; +import { fetch as fetchSession } 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 LivePlayer from './LivePlayer'; + +const SESSIONS_ROUTE = sessionsRoute(); + +function LiveSession({ + sessionId, + loading, + hasErrors, + session, + fetchSession, + fetchSlackList, + hasSessionsPath + }) { + usePageTitle("OpenReplay Assist"); + + useEffect(() => { + fetchSlackList() + }, []); + + useEffect(() => { + if (sessionId != null) { + fetchSession(sessionId) + } else { + console.error("No sessionID in route.") + } + return () => { + if (!session.exists()) return; + } + },[ sessionId, hasSessionsPath ]); + + return ( + + + + ); +} + +export default withPermissions(['ASSIST_LIVE'], '', true)(connect((state, props) => { + const { match: { params: { sessionId } } } = props; + const isAssist = state.getIn(['sessions', 'activeTab']).type === 'live'; + const hasSessiosPath = state.getIn([ 'sessions', 'sessionPath' ]).includes('/sessions'); + return { + sessionId, + loading: state.getIn([ 'sessions', 'loading' ]), + hasErrors: !!state.getIn([ 'sessions', 'errors' ]), + session: state.getIn([ 'sessions', 'current' ]), + hasSessionsPath: hasSessiosPath && !isAssist, + }; +}, { + fetchSession, + fetchSlackList, +})(LiveSession)); \ No newline at end of file diff --git a/frontend/app/components/Session_/Player/Controls/Controls.js b/frontend/app/components/Session_/Player/Controls/Controls.js index b21d45f77..464165e69 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.js +++ b/frontend/app/components/Session_/Player/Controls/Controls.js @@ -106,6 +106,7 @@ function getStorageName(type) { bottomBlock: state.getIn([ 'components', 'player', 'bottomBlock' ]), showStorage: props.showStorage || !state.getIn(['components', 'player', 'hiddenHints', 'storage']), showStack: props.showStack || !state.getIn(['components', 'player', 'hiddenHints', 'stack']), + closedLive: !!state.getIn([ 'sessions', 'errors' ]), } }, { fullscreenOn, @@ -253,7 +254,8 @@ export default class Controls extends React.Component { showExceptions, fullscreen, skipToIssue, - inspectorMode + inspectorMode, + closedLive, } = this.props; // const inspectorMode = bottomBlock === INSPECTOR; @@ -263,30 +265,35 @@ export default class Controls extends React.Component { { !live && } { !fullscreen &&
- { !live ? -
- { this.renderPlayBtn() } - - -
- : -
- - {'Elapsed'} - -
- } +
+ { !live && ( +
+ { this.renderPlayBtn() } + + +
+ )} + + { live && !closedLive && ( +
+ + {'Elapsed'} + +
+ )} +
+
{!live && diff --git a/frontend/app/components/Session_/Player/Overlay.tsx b/frontend/app/components/Session_/Player/Overlay.tsx index bed0e3a00..994608108 100644 --- a/frontend/app/components/Session_/Player/Overlay.tsx +++ b/frontend/app/components/Session_/Player/Overlay.tsx @@ -25,6 +25,7 @@ interface Props { nextId: string, togglePlay: () => void, + closedLive?: boolean } function Overlay({ @@ -41,6 +42,7 @@ function Overlay({ activeTargetIndex, nextId, togglePlay, + closedLive }: Props) { // useEffect(() =>{ @@ -56,7 +58,7 @@ function Overlay({ <> { showAutoplayTimer && } { showLiveStatusText && - + } { messagesLoading && } { showPlayIconLayer && diff --git a/frontend/app/components/Session_/Player/Overlay/LiveStatusText.tsx b/frontend/app/components/Session_/Player/Overlay/LiveStatusText.tsx index b642a8a7c..3f24901de 100644 --- a/frontend/app/components/Session_/Player/Overlay/LiveStatusText.tsx +++ b/frontend/app/components/Session_/Player/Overlay/LiveStatusText.tsx @@ -12,6 +12,14 @@ interface Props { export default function LiveStatusText({ text, concetionStatus }: Props) { const renderView = () => { switch (concetionStatus) { + case ConnectionStatus.Closed: + return ( +
+
Session not found
+
The remote session doesn’t exist anymore.
The user may have closed the tab/browser while you were trying to establish a connection.
+
+ ) + case ConnectionStatus.Connecting: return (
diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index baae88ea3..a3a6e3b1a 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { findDOMNode } from 'react-dom'; import cn from 'classnames'; -import { Loader, IconButton, EscapeButton } from 'UI'; +import { EscapeButton } from 'UI'; import { hide as hideTargetDefiner } from 'Duck/components/targetDefiner'; import { fullscreenOff } from 'Duck/components/player'; import { attach as attachPlayer, Controls as PlayerControls, connectPlayer } from 'Player'; @@ -10,14 +10,13 @@ import Overlay from './Overlay'; import stl from './player.css'; import EventsToggleButton from '../../Session/EventsToggleButton'; - @connectPlayer(state => ({ live: state.live, })) @connect(state => ({ - //session: state.getIn([ 'sessions', 'current' ]), fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]), nextId: state.getIn([ 'sessions', 'nextId' ]), + closedLive: !!state.getIn([ 'sessions', 'errors' ]), }), { hideTargetDefiner, fullscreenOff, @@ -26,6 +25,8 @@ export default class Player extends React.PureComponent { screenWrapper = React.createRef(); componentDidMount() { + if (this.props.closedLive) return; + const parentElement = findDOMNode(this.screenWrapper.current); //TODO: good architecture attachPlayer(parentElement); } @@ -38,6 +39,7 @@ export default class Player extends React.PureComponent { fullscreenOff, nextId, live, + closedLive, } = this.props; return ( @@ -45,12 +47,10 @@ export default class Player extends React.PureComponent { className={ cn(className, stl.playerBody, "flex flex-col relative") } data-bottom-block={ bottomBlockIsActive } > - { fullscreen && - - } + {fullscreen && } {!live && !fullscreen && }
- +
({ fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]), bottomBlock: state.getIn([ 'components', 'player', 'bottomBlock' ]), + closedLive: !!state.getIn([ 'sessions', 'errors' ]), })) export default class PlayerBlock extends React.PureComponent { componentDidUpdate(prevProps) { @@ -44,13 +44,14 @@ export default class PlayerBlock extends React.PureComponent { } render() { - const { fullscreen, bottomBlock } = this.props; + const { fullscreen, bottomBlock, closedLive } = this.props; return (
{ !fullscreen && !!bottomBlock &&
diff --git a/frontend/app/components/Session_/PlayerBlockHeader.js b/frontend/app/components/Session_/PlayerBlockHeader.js index d352db2b4..6e274d4b1 100644 --- a/frontend/app/components/Session_/PlayerBlockHeader.js +++ b/frontend/app/components/Session_/PlayerBlockHeader.js @@ -2,12 +2,12 @@ import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; import { browserIcon, osIcon, deviceTypeIcon } from 'App/iconNames'; import { formatTimeOrDate } from 'App/date'; -import { sessions as sessionsRoute, withSiteId } from 'App/routes'; +import { sessions as sessionsRoute, assist as assistRoute, withSiteId } from 'App/routes'; import { Icon, CountryFlag, IconButton, BackLink, Popup } from 'UI'; import { toggleFavorite, setSessionPath } from 'Duck/sessions'; import cn from 'classnames'; import { connectPlayer } from 'Player'; -import HeaderInfo from './HeaderInfo'; +// import HeaderInfo from './HeaderInfo'; import SharePopup from '../shared/SharePopup/SharePopup'; import { fetchList as fetchListIntegration } from 'Duck/integrations/actions'; import { countries } from 'App/constants'; @@ -21,10 +21,7 @@ import AssistTabs from '../Assist/components/AssistTabs'; import SessionInfoItem from './SessionInfoItem' const SESSIONS_ROUTE = sessionsRoute(); - -function capitalise(str) { - return str[0].toUpperCase() + str.slice(1); -} +const ASSIST_ROUTE = assistRoute(); @connectPlayer(state => ({ width: state.width, height: state.height, @@ -46,6 +43,7 @@ function capitalise(str) { siteId: state.getIn([ 'user', 'siteId' ]), hasSessionsPath: hasSessioPath && !isAssist, metaList: state.getIn(['customFields', 'list']).map(i => i.key), + closedLive: !!state.getIn([ 'sessions', 'errors' ]), } }, { toggleFavorite, fetchListIntegration, setSessionPath @@ -67,8 +65,9 @@ export default class PlayerBlockHeader extends React.PureComponent { backHandler = () => { const { history, siteId, sessionPath } = this.props; - if (sessionPath === history.location.pathname || sessionPath.includes("/session/")) { - history.push(withSiteId(SESSIONS_ROUTE), siteId); + const isLiveSession = sessionPath.includes("/assist"); + if (sessionPath === history.location.pathname || sessionPath.includes("/session/") || isLiveSession) { + history.push(withSiteId(isLiveSession ? ASSIST_ROUTE: SESSIONS_ROUTE, siteId)); } else { history.push(sessionPath ? sessionPath : withSiteId(SESSIONS_ROUTE, siteId)); } @@ -107,6 +106,7 @@ export default class PlayerBlockHeader extends React.PureComponent { hasSessionsPath, sessionPath, metaList, + closedLive = false, } = this.props; const _live = live && !hasSessionsPath; @@ -122,20 +122,8 @@ export default class PlayerBlockHeader extends React.PureComponent {
{ _live && } - - {/*
- -
- { formatTimeOrDate(startedAt) } { this.props.local === 'UTC' ? 'UTC' : ''} -
-
- - - - */} - -
+
{ live && hasSessionsPath && ( <>
this.props.setSessionPath('')}> diff --git a/frontend/app/components/shared/SessionItem/SessionItem.js b/frontend/app/components/shared/SessionItem/SessionItem.js index 324c6b723..023c9c724 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.js +++ b/frontend/app/components/shared/SessionItem/SessionItem.js @@ -11,7 +11,7 @@ import { } from 'UI'; import { deviceTypeIcon } from 'App/iconNames'; import { toggleFavorite, setSessionPath } from 'Duck/sessions'; -import { session as sessionRoute, withSiteId } from 'App/routes'; +import { session as sessionRoute, liveSession as liveSessionRoute, withSiteId } from 'App/routes'; import { durationFormatted, formatTimeOrDate } from 'App/date'; import stl from './sessionItem.css'; import LiveTag from 'Shared/LiveTag'; @@ -136,7 +136,7 @@ export default class SessionItem extends React.PureComponent {
- +
diff --git a/frontend/app/player/MessageDistributor/managers/AssistManager.ts b/frontend/app/player/MessageDistributor/managers/AssistManager.ts index 0b09469a1..51895b327 100644 --- a/frontend/app/player/MessageDistributor/managers/AssistManager.ts +++ b/frontend/app/player/MessageDistributor/managers/AssistManager.ts @@ -26,6 +26,7 @@ export enum ConnectionStatus { Inactive, Disconnected, Error, + Closed, } export enum RemoteControlStatus { @@ -37,6 +38,8 @@ export enum RemoteControlStatus { export function getStatusText(status: ConnectionStatus): string { switch(status) { + case ConnectionStatus.Closed: + return 'Closed...'; case ConnectionStatus.Connecting: return "Connecting..."; case ConnectionStatus.Connected: diff --git a/frontend/app/routes.js b/frontend/app/routes.js index 0f10950df..f9987d49c 100644 --- a/frontend/app/routes.js +++ b/frontend/app/routes.js @@ -85,7 +85,8 @@ export const sessions = params => queried('/sessions', params); export const assist = params => queried('/assist', params); export const session = (sessionId = ':sessionId', hash) => hashed(`/session/${ sessionId }`, hash); -export const liveSession = (sessionId = ':sessionId', hash) => hashed(`/live/session/${ sessionId }`, hash); +export const liveSession = (sessionId = ':sessionId', hash) => hashed(`/assist/${ sessionId }`, hash); +// export const liveSession = (sessionId = ':sessionId', hash) => hashed(`/live/session/${ sessionId }`, hash); export const errors = params => queried('/errors', params); export const error = (id = ':errorId', hash) => hashed(`/errors/${ id }`, hash); @@ -132,7 +133,6 @@ export function isRoute(route, path){ const SITE_CHANGE_AVALIABLE_ROUTES = [ sessions(), assist(), dashboard(), errors(), onboarding('')]; export const siteChangeAvaliable = path => SITE_CHANGE_AVALIABLE_ROUTES.some(r => isRoute(r, path)); - export const redirects = Object.entries({ [ client('custom-fields') ]: client(CLIENT_TABS.CUSTOM_FIELDS), }); \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 2058dcc16..e15ec72e9 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -13,7 +13,7 @@ module.exports = { //'appearance', // 'backgroundAttachment', 'backgroundColor', - // 'backgroundOpacity', + 'backgroundOpacity', // 'backgroundPosition', // 'backgroundRepeat', // 'backgroundSize',