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 }
: