diff --git a/frontend/app/Router.js b/frontend/app/Router.js index 86d878a1c..04a0c5712 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -6,7 +6,6 @@ import { Notification } from 'UI'; import { Loader } from 'UI'; import { fetchUserInfo } from 'Duck/user'; import withSiteIdUpdater from 'HOCs/withSiteIdUpdater'; -import WidgetViewPure from 'Components/Dashboard/components/WidgetView'; import Header from 'Components/Header/Header'; import { fetchList as fetchSiteList } from 'Duck/site'; import { fetchList as fetchAnnouncements } from 'Duck/announcements'; @@ -37,6 +36,7 @@ const ErrorsPure = lazy(() => import('Components/Errors/Errors')); const FunnelDetailsPure = lazy(() => import('Components/Funnels/FunnelDetails')); const FunnelIssueDetails = lazy(() => import('Components/Funnels/FunnelIssueDetails')); const FunnelPagePure = lazy(() => import('Components/Funnels/FunnelPage')); +const MultiviewPure = lazy(() => import('Components/Session_/Multiview/Multiview.tsx')); const BugFinder = withSiteIdUpdater(BugFinderPure); const Dashboard = withSiteIdUpdater(DashboardPure); @@ -49,6 +49,7 @@ const Errors = withSiteIdUpdater(ErrorsPure); const FunnelPage = withSiteIdUpdater(FunnelPagePure); const FunnelsDetails = withSiteIdUpdater(FunnelDetailsPure); const FunnelIssue = withSiteIdUpdater(FunnelIssueDetails); +const Multiview = withSiteIdUpdater(MultiviewPure) const withSiteId = routes.withSiteId; const METRICS_PATH = routes.metrics(); @@ -81,6 +82,7 @@ const FORGOT_PASSWORD = routes.forgotPassword(); const CLIENT_PATH = routes.client(); const ONBOARDING_PATH = routes.onboarding(); const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB); +const MULTIVIEW_PATH = routes.multiview(); @withStore @withRouter @@ -172,7 +174,7 @@ class Router extends React.Component { const { isLoggedIn, jwt, siteId, sites, loading, changePassword, location, existingTenant, onboarding, isEnterprise } = this.props; const siteIdList = sites.map(({ id }) => id).toJS(); const hideHeader = (location.pathname && location.pathname.includes('/session/')) || location.pathname.includes('/assist/'); - const isPlayer = isRoute(SESSION_PATH, location.pathname) || isRoute(LIVE_SESSION_PATH, location.pathname); + const isPlayer = isRoute(SESSION_PATH, location.pathname) || isRoute(LIVE_SESSION_PATH, location.pathname) || isRoute(MULTIVIEW_PATH, location.pathname); const redirectToOnboarding = !onboarding && localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true' return isLoggedIn ? ( @@ -219,6 +221,7 @@ class Router extends React.Component { + diff --git a/frontend/app/components/Session/LivePlayer.tsx b/frontend/app/components/Session/LivePlayer.tsx index c6c0ead8a..5a88cf89a 100644 --- a/frontend/app/components/Session/LivePlayer.tsx +++ b/frontend/app/components/Session/LivePlayer.tsx @@ -6,7 +6,7 @@ import withRequest from 'HOCs/withRequest'; import withPermissions from 'HOCs/withPermissions'; import { PlayerContext, defaultContextValue } from './playerContext'; import { makeAutoObservable } from 'mobx'; -import { createLiveWebPlayer } from 'Player' +import { createLiveWebPlayer } from 'Player'; import PlayerBlockHeader from '../Session_/PlayerBlockHeader'; import PlayerBlock from '../Session_/PlayerBlock'; import styles from '../Session_/session.module.css'; @@ -23,6 +23,8 @@ interface Props { isEnterprise: boolean; userEmail: string; userName: string; + isMultiview?: boolean; + customSession?: Session; } function LivePlayer({ @@ -36,34 +38,39 @@ function LivePlayer({ isEnterprise, userEmail, userName, + isMultiview, + customSession, }: Props) { const [contextValue, setContextValue] = useState(defaultContextValue); const [fullView, setFullView] = useState(false); + // @ts-ignore burn immutable + const usedSession = isMultiview ? customSession : session.toJS(); + useEffect(() => { - if (loadingCredentials || !session.sessionId) return; + if (loadingCredentials || !usedSession.sessionId) return; const sessionWithAgentData = { - // @ts-ignore burn immutable - ...session.toJS(), + ...usedSession, agentInfo: { email: userEmail, name: userName, }, - } - const [player, store] = createLiveWebPlayer( - sessionWithAgentData, - assistCredendials, - (state) => makeAutoObservable(state) - ) - setContextValue({ player, store }) - player.play() - return () => player.clean() - }, [ session.sessionId, assistCredendials ]) + }; + const [player, store] = createLiveWebPlayer(sessionWithAgentData, assistCredendials, (state) => + makeAutoObservable(state) + ); + setContextValue({ player, store }); + player.play(); + return () => player.clean(); + }, [session.sessionId, assistCredendials]); // LAYOUT (TODO: local layout state - useContext or something..) useEffect(() => { const queryParams = new URLSearchParams(window.location.search); - if (queryParams.has('fullScreen') && queryParams.get('fullScreen') === 'true') { + if ( + (queryParams.has('fullScreen') && queryParams.get('fullScreen') === 'true') || + location.pathname.includes('multiview') + ) { setFullView(true); } @@ -95,8 +102,15 @@ function LivePlayer({ fullscreen={fullscreen} /> )} -
- +
+
); diff --git a/frontend/app/components/Session_/Multiview/Multiview.tsx b/frontend/app/components/Session_/Multiview/Multiview.tsx new file mode 100644 index 000000000..b60c20c27 --- /dev/null +++ b/frontend/app/components/Session_/Multiview/Multiview.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { useStore } from 'App/mstore'; +import { BackLink, Icon } from 'UI' +import { observer } from 'mobx-react-lite'; +import { connect } from 'react-redux'; +import { fetchSessions } from 'Duck/liveSearch'; +import { useHistory } from 'react-router-dom'; +import { liveSession, assist, withSiteId } from 'App/routes'; +import AssistSessionsModal from 'App/components/Session_/Player/Controls/AssistSessionsModal'; +import { useModal } from 'App/components/Modal'; +import LivePlayer from 'App/components/Session/LivePlayer'; + +function Multiview({ total, fetchSessions, siteId }: { total: number; fetchSessions: (filter: any) => void, siteId: string }) { + const { showModal, hideModal } = useModal(); + + const { assistMultiviewStore } = useStore(); + const history = useHistory(); + + React.useEffect(() => { + if (total === 0) { + fetchSessions({}); + } + }, []); + + const openLiveSession = (e: React.MouseEvent, sessionId: string) => { + e.stopPropagation() + assistMultiviewStore.setActiveSession(sessionId); + history.push(withSiteId(liveSession(sessionId), siteId)); + }; + const openList = () => { + history.push(withSiteId(assist(), siteId)) + } + const openListModal = () => { + showModal(, { right: true }); + } + const replaceSession = (e: React.MouseEvent, sessionId: string) => { + e.stopPropagation() + showModal(, { right: true }); + } + const deleteSession = (e: React.MouseEvent, sessionId: string) => { + e.stopPropagation() + assistMultiviewStore.removeSession(sessionId) + } + + const placeholder = new Array(4 - assistMultiviewStore.sessions.length).fill(0); + + return ( +
+
+ {/* @ts-ignore */} +
+
{`Watching ${assistMultiviewStore.sessions.length} of ${total} Live Sessions`}
+
+
+ {assistMultiviewStore.sortedSessions.map((session) => ( +
+
openLiveSession(e, session.sessionId)} className="w-full h-full"> + +
+
+
{session.userDisplayName}
+
+
replaceSession(e, session.sessionId)}>Replace Session
+
deleteSession(e, session.sessionId)}> + +
+
+
+
+ ))} + {placeholder.map((_, i) => ( +
+ Add Session +
+ ))} +
+
+ ); +} + +export default connect( + (state: any) => ({ + total: state.getIn(['liveSearch', 'total']), + siteId: state.getIn([ 'site', 'siteId' ]) + }), + { + fetchSessions, + } +)(observer(Multiview)); diff --git a/frontend/app/components/Session_/Player/Controls/AssistSessionsModal/AssistSessionsModal.tsx b/frontend/app/components/Session_/Player/Controls/AssistSessionsModal/AssistSessionsModal.tsx index 9919f85f5..ba760158a 100644 --- a/frontend/app/components/Session_/Player/Controls/AssistSessionsModal/AssistSessionsModal.tsx +++ b/frontend/app/components/Session_/Player/Controls/AssistSessionsModal/AssistSessionsModal.tsx @@ -27,6 +27,7 @@ interface ConnectProps { metaList: any; sort: any; total: number; + replaceTarget?: string; addFilterByKeyAndValue: (key: FilterKey, value: string) => void; updateCurrentPage: (page: number) => void; applyFilter: (filter: any) => void; @@ -36,7 +37,7 @@ interface ConnectProps { type Props = OwnProps & ConnectProps; function AssistSessionsModal(props: Props) { - const { assistTabStore } = useStore(); + const { assistMultiviewStore } = useStore(); const { loading, list, metaList = [], filter, currentPage, total, onAdd } = props; const onUserClick = () => false; const { filters } = filter; @@ -56,7 +57,11 @@ function AssistSessionsModal(props: Props) { props.applyFilter({ sort: value.value }); }; const onSessionAdd = (session: Session) => { - assistTabStore.addSession(session); + if (props.replaceTarget) { + assistMultiviewStore.replaceSession(props.replaceTarget, session) + } else { + assistMultiviewStore.addSession(session); + } onAdd() } @@ -87,7 +92,7 @@ function AssistSessionsModal(props: Props) { <> {list.map((session) => ( -
+
s.sessionId === session.sessionId) !== -1 ? 'cursor-not-allowed' : '')}> s.sessionId === session.sessionId) !== -1} isAdd onClick={() => onSessionAdd(session)} /> diff --git a/frontend/app/components/Session_/Player/Controls/AssistSessionsTabs/AssistSessionsTabs.tsx b/frontend/app/components/Session_/Player/Controls/AssistSessionsTabs/AssistSessionsTabs.tsx index 362c87596..f73e8f5b2 100644 --- a/frontend/app/components/Session_/Player/Controls/AssistSessionsTabs/AssistSessionsTabs.tsx +++ b/frontend/app/components/Session_/Player/Controls/AssistSessionsTabs/AssistSessionsTabs.tsx @@ -1,22 +1,20 @@ import React from 'react'; import cn from 'classnames'; -import { useModal } from 'App/components/Modal'; import { Icon } from 'UI'; -import AssistSessionsModal from '../AssistSessionsModal'; -import { useStore } from 'App/mstore' -import Session from 'App/mstore/types/session' -import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; +import { useHistory } from 'react-router-dom'; +import { multiview, liveSession, withSiteId } from 'App/routes'; +import { connect } from 'react-redux'; interface ITab { onClick?: () => void; - onDoubleClick?: () => void; classNames?: string; children: React.ReactNode; } const Tab = (props: ITab) => (
@@ -24,13 +22,13 @@ const Tab = (props: ITab) => (
); -const InactiveTab = (props: Omit) => ( +const InactiveTab = (props: Omit) => ( ); const ActiveTab = (props: Omit) => ( - + ); @@ -40,37 +38,47 @@ const CurrentTab = () => ( ); -function AssistTabs({ session }: { session: Session }) { - const { showModal, hideModal } = useModal(); - const { assistTabStore } = useStore() +function AssistTabs({ session, siteId }: { session: Record; siteId: string }) { + const history = useHistory(); + const { assistMultiviewStore } = useStore(); - const placeholder = new Array(4 - assistTabStore.sessions.length).fill(0) + const placeholder = new Array(4 - assistMultiviewStore.sessions.length).fill(0); React.useEffect(() => { - if (assistTabStore.sessions.length === 0) { - assistTabStore.addSession(session) - assistTabStore.setActiveSession(session.sessionId) - console.log(assistTabStore.sessions, assistTabStore.activeSession, session.sessionId) + if (assistMultiviewStore.sessions.length === 0) { + assistMultiviewStore.setDefault(session); } - }, []) + }, []); - const showAssistList = () => showModal(, { right: true }); + const openGrid = () => { + history.push(withSiteId(multiview(), siteId)); + }; + const openLiveSession = (sessionId: string) => { + assistMultiviewStore.setActiveSession(sessionId); + history.push(withSiteId(liveSession(sessionId), siteId)); + }; + + console.log(assistMultiviewStore.activeSessionId) return (
- {assistTabStore.sessions.map(session => ( - - {assistTabStore.isActive(session.sessionId) - ? : assistTabStore.setActiveSession(session.sessionId)} />} + {assistMultiviewStore.sortedSessions.map((session) => ( + + {assistMultiviewStore.isActive(session.sessionId) ? ( + + ) : ( + openLiveSession(session.sessionId)} /> + )} - ) - )} + ))} {placeholder.map((_, i) => ( - + ))}
); } -export default observer(AssistTabs); +export default connect((state: any) => ({ siteId: state.getIn(['site', 'siteId']) }))( + observer(AssistTabs) +); diff --git a/frontend/app/components/Session_/Player/Controls/Controls.tsx b/frontend/app/components/Session_/Player/Controls/Controls.tsx index 33dbf1719..eb283c47a 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.tsx +++ b/frontend/app/components/Session_/Player/Controls/Controls.tsx @@ -23,6 +23,7 @@ import { } from 'Duck/components/player'; import { PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; +import { fetchSessions } from 'Duck/liveSearch'; import { AssistDuration } from './Time'; import Timeline from './Timeline'; @@ -98,8 +99,11 @@ function Controls(props: any) { showStorageRedux, session, // showStackRedux, + fetchSessions: fetchAssistSessions, + totalAssistSessions, } = props; + const isAssist = window.location.pathname.includes('/assist/'); const storageType = selectStorageType(store.get()); const disabled = disabledRedux || cssLoading || messagesLoading || inspectorMode || markedTargets; const profilesCount = profilesList.length; @@ -141,6 +145,9 @@ function Controls(props: any) { React.useEffect(() => { document.addEventListener('keydown', onKeyDown.bind(this)); + if (isAssist && totalAssistSessions === 0) { + fetchAssistSessions(); + } return () => { document.removeEventListener('keydown', onKeyDown.bind(this)); }; @@ -148,12 +155,12 @@ function Controls(props: any) { const forthTenSeconds = () => { // @ts-ignore - player.jumpInterval(SKIP_INTERVALS[skipInterval]) + player.jumpInterval(SKIP_INTERVALS[skipInterval]); }; const backTenSeconds = () => { // @ts-ignore - player.jumpInterval(-SKIP_INTERVALS[skipInterval]) + player.jumpInterval(-SKIP_INTERVALS[skipInterval]); }; const renderPlayBtn = () => { @@ -261,9 +268,11 @@ function Controls(props: any) { )}
-
- -
+ {isAssist && totalAssistSessions > 1 ? ( +
+ +
+ ) : null}
} {bottomBlock === CONSOLE && } {bottomBlock === NETWORK && ( - // )} {/* {bottomBlock === STACKEVENTS && } */} @@ -93,7 +93,7 @@ function Player(props) { {bottomBlock === INSPECTOR && }
)} - {!fullView && - {!fullscreen && !fullView && ( +
+ {!fullscreen && !fullView && !isMultiview && ( )}
); diff --git a/frontend/app/components/ui/BackLink/BackLink.js b/frontend/app/components/ui/BackLink/BackLink.js index a150865bb..93445cce6 100644 --- a/frontend/app/components/ui/BackLink/BackLink.js +++ b/frontend/app/components/ui/BackLink/BackLink.js @@ -4,7 +4,7 @@ import cls from './backLink.module.css'; import cn from 'classnames'; export default function BackLink ({ - className, to, onClick, label, vertical = false, style + className, to, onClick, label = '', vertical = false, style }) { const children = (
@@ -18,7 +18,7 @@ export default function BackLink ({ className={ verticalClassName } to={ to } > - { children } + { children } :