change(ui): extract Live Player and its components

This commit is contained in:
nick-delirium 2023-01-10 12:59:14 +01:00 committed by Delirium
parent 36be728a54
commit 6334f10888
43 changed files with 2545 additions and 349 deletions

View file

@ -2,9 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Button, Tooltip } from 'UI';
import { connect } from 'react-redux';
import cn from 'classnames';
import { toggleChatWindow } from 'Duck/sessions';
import ChatWindow from '../../ChatWindow';
// state enums
import {
CallingState,
ConnectionStatus,
@ -12,7 +10,7 @@ import {
RequestLocalStream,
} from 'Player';
import type { LocalStream } from 'Player';
import { PlayerContext } from 'App/components/Session/playerContext';
import { PlayerContext, ILivePlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { toast } from 'react-toastify';
import { confirm } from 'UI';
@ -30,15 +28,10 @@ function onError(e: any) {
interface Props {
userId: string;
calling: CallingState;
annotating: boolean;
peerConnectionStatus: ConnectionStatus;
remoteControlStatus: RemoteControlStatus;
hasPermission: boolean;
isEnterprise: boolean;
isCallActive: boolean;
agentIds: string[];
livePlay: boolean;
userDisplayName: string;
}
@ -50,7 +43,8 @@ function AssistActions({
agentIds,
userDisplayName,
}: Props) {
const { player, store } = React.useContext(PlayerContext)
// @ts-ignore ???
const { player, store } = React.useContext<ILivePlayerContext>(PlayerContext)
const {
assistManager: {
@ -123,6 +117,7 @@ function AssistActions({
const addIncomeStream = (stream: MediaStream) => {
setIncomeStream((oldState) => {
if (oldState === null) return [stream]
if (!oldState.find((existingStream) => existingStream.id === stream.id)) {
return [...oldState, stream];
}
@ -257,8 +252,7 @@ const con = connect(
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
userDisplayName: state.getIn(['sessions', 'current']).userDisplayName,
};
},
{ toggleChatWindow }
}
);
export default con(

View file

@ -1,41 +1,34 @@
import React from 'react';
import { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player';
import withRequest from 'HOCs/withRequest';
import withPermissions from 'HOCs/withPermissions';
import { PlayerContext, defaultContextValue } from './playerContext';
import { PlayerContext, defaultContextValue, ILivePlayerContext } from './playerContext';
import { makeAutoObservable } from 'mobx';
import { createLiveWebPlayer } from 'Player';
import PlayerBlockHeader from '../Session_/PlayerBlockHeader';
import PlayerBlock from '../Session_/PlayerBlock';
import PlayerBlockHeader from './Player/LivePlayer/LivePlayerBlockHeader';
import PlayerBlock from './Player/LivePlayer/LivePlayerBlock';
import styles from '../Session_/session.module.css';
import Session from 'App/mstore/types/session';
import withLocationHandlers from 'HOCs/withLocationHandlers';
interface Props {
session: Session;
fullscreen: boolean;
loadingCredentials: boolean;
assistCredendials: RTCIceServer[];
assistCredentials: RTCIceServer[];
isEnterprise: boolean;
userEmail: string;
userName: string;
customSession?: Session;
isMultiview?: boolean;
query?: Record<string, (key: string) => any>;
toggleFullscreen: (isOn: boolean) => void;
closeBottomBlock: () => void;
request: () => void;
}
function LivePlayer({
session,
toggleFullscreen,
closeBottomBlock,
fullscreen,
loadingCredentials,
assistCredendials,
assistCredentials,
request,
isEnterprise,
userEmail,
@ -44,11 +37,11 @@ function LivePlayer({
customSession,
query
}: Props) {
const [contextValue, setContextValue] = useState(defaultContextValue);
// @ts-ignore
const [contextValue, setContextValue] = useState<ILivePlayerContext>(defaultContextValue);
const [fullView, setFullView] = useState(false);
const openedFromMultiview = query.get('multi') === 'true'
// @ts-ignore burn immutable
const usedSession = isMultiview ? customSession : session.toJS();
const openedFromMultiview = query?.get('multi') === 'true'
const usedSession = isMultiview ? customSession! : session;
useEffect(() => {
if (loadingCredentials || !usedSession.sessionId) return;
@ -59,13 +52,13 @@ function LivePlayer({
name: userName,
},
};
const [player, store] = createLiveWebPlayer(sessionWithAgentData, assistCredendials, (state) =>
const [player, store] = createLiveWebPlayer(sessionWithAgentData, assistCredentials, (state) =>
makeAutoObservable(state)
);
setContextValue({ player, store });
return () => player.clean();
}, [session.sessionId, assistCredendials]);
}, [session.sessionId, assistCredentials]);
// LAYOUT (TODO: local layout state - useContext or something..)
useEffect(() => {
@ -80,10 +73,6 @@ function LivePlayer({
if (isEnterprise) {
request();
}
return () => {
toggleFullscreen(false);
closeBottomBlock();
};
}, []);
const TABS = {
@ -102,7 +91,6 @@ function LivePlayer({
activeTab={activeTab}
setActiveTab={setActiveTab}
tabs={TABS}
fullscreen={fullscreen}
isMultiview={openedFromMultiview}
/>
)}
@ -112,7 +100,6 @@ function LivePlayer({
height: isMultiview ? '100%' : undefined,
width: isMultiview ? '100%' : undefined,
}}
data-fullscreen={fullscreen}
>
<PlayerBlock isMultiview={isMultiview} />
</div>
@ -123,7 +110,7 @@ function LivePlayer({
export default withRequest({
initialData: null,
endpoint: '/assist/credentials',
dataName: 'assistCredendials',
dataName: 'assistCredentials',
loadingName: 'loadingCredentials',
})(
withPermissions(
@ -136,13 +123,11 @@ export default withRequest({
return {
session: state.getIn(['sessions', 'current']),
showAssist: state.getIn(['sessions', 'showChatWindow']),
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
userEmail: state.getIn(['user', 'account', 'email']),
userName: state.getIn(['user', 'account', 'name']),
};
},
{ toggleFullscreen, closeBottomBlock }
}
)(withLocationHandlers()(LivePlayer))
)
);

View file

@ -0,0 +1,28 @@
import React from 'react';
import { Duration } from 'luxon';
import { PlayerContext, ILivePlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
const AssistDurationCont = () => {
// @ts-ignore ??? TODO
const { store } = React.useContext<ILivePlayerContext>(PlayerContext)
const { assistStart } = store.get()
const [assistDuration, setAssistDuration] = React.useState('00:00');
React.useEffect(() => {
const interval = setInterval(() => {
setAssistDuration(Duration.fromMillis(+new Date() - assistStart).toFormat('mm:ss'));
}
, 1000);
return () => clearInterval(interval);
}, [])
return (
<>
Elapsed {assistDuration}
</>
)
}
const AssistDuration = observer(AssistDurationCont)
export default AssistDuration;

View file

@ -0,0 +1,86 @@
import React from 'react';
import cn from 'classnames';
import { Icon } from 'UI';
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;
classNames?: string;
children: React.ReactNode;
}
const Tab = (props: ITab) => (
<div
onClick={props.onClick}
className={cn('p-1 rounded flex items-center justify-center cursor-pointer', props.classNames)}
>
{props.children}
</div>
);
export const InactiveTab = React.memo((props: Omit<ITab, 'children'>) => (
<Tab onClick={props.onClick} classNames={cn("hover:bg-gray-bg bg-gray-light", props.classNames)}>
<Icon name="plus" size="22" color="white" />
</Tab>
));
const ActiveTab = React.memo((props: Omit<ITab, 'children'>) => (
<Tab onClick={props.onClick} classNames="hover:bg-teal bg-borderColor-primary">
<Icon name="play-fill-new" size="22" color="white" />
</Tab>
));
const CurrentTab = React.memo(() => (
<Tab classNames="bg-teal color-white">
<span style={{ fontSize: '0.65rem' }}>PLAYING</span>
</Tab>
));
function AssistTabs({ session, siteId }: { session: Record<string, any>; siteId: string }) {
const history = useHistory();
const { assistMultiviewStore } = useStore();
const placeholder = new Array(4 - assistMultiviewStore.sessions.length).fill(0);
React.useEffect(() => {
if (assistMultiviewStore.sessions.length === 0) {
assistMultiviewStore.setDefault(session);
}
}, []);
const openGrid = () => {
const sessionIdQuery = encodeURIComponent(assistMultiviewStore.sessions.map((s) => s.sessionId).join(','));
return history.push(withSiteId(multiview(sessionIdQuery), siteId));
};
const openLiveSession = (sessionId: string) => {
assistMultiviewStore.setActiveSession(sessionId);
history.push(withSiteId(liveSession(sessionId), siteId));
};
return (
<div className="grid grid-cols-2 w-28 h-full" style={{ gap: '4px' }}>
{assistMultiviewStore.sortedSessions.map((session: { key: number, sessionId: string }) => (
<React.Fragment key={session.key}>
{assistMultiviewStore.isActive(session.sessionId) ? (
<CurrentTab />
) : (
<ActiveTab onClick={() => openLiveSession(session.sessionId)} />
)}
</React.Fragment>
))}
{placeholder.map((_, i) => (
<React.Fragment key={i}>
<InactiveTab onClick={openGrid} />
</React.Fragment>
))}
</div>
);
}
export default connect((state: any) => ({ siteId: state.getIn(['site', 'siteId']) }))(
observer(AssistTabs)
);

View file

@ -0,0 +1 @@
export { default, InactiveTab } from './AssistSessionsTabs'

View file

@ -0,0 +1,131 @@
import React from 'react';
import cn from 'classnames';
import { connect } from 'react-redux';
import LiveTag from 'Shared/LiveTag';
import AssistSessionsTabs from './AssistSessionsTabs';
import {
CONSOLE, toggleBottomBlock,
} from 'Duck/components/player';
import { PlayerContext, ILivePlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { fetchSessions } from 'Duck/liveSearch';
import AssistDuration from './AssistDuration';
import Timeline from './Timeline';
import ControlButton from 'Components/Session_/Player/Controls/ControlButton';
import styles from 'Components/Session_/Player/Controls/controls.module.css';
function Controls(props: any) {
// @ts-ignore ?? TODO
const { player, store } = React.useContext<ILivePlayerContext>(PlayerContext);
const { jumpToLive } = player;
const {
livePlay,
logMarkedCountNow: logRedCount,
exceptionsList,
} = store.get();
const showExceptions = exceptionsList.length > 0;
const {
bottomBlock,
toggleBottomBlock,
closedLive,
skipInterval,
session,
fetchSessions: fetchAssistSessions,
totalAssistSessions,
} = props;
const onKeyDown = (e: any) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
if (e.key === 'ArrowRight') {
forthTenSeconds();
}
if (e.key === 'ArrowLeft') {
backTenSeconds();
}
};
React.useEffect(() => {
document.addEventListener('keydown', onKeyDown.bind(this));
if (totalAssistSessions === 0) {
fetchAssistSessions();
}
return () => {
document.removeEventListener('keydown', onKeyDown.bind(this));
};
}, []);
const forthTenSeconds = () => {
// @ts-ignore
player.jumpInterval(SKIP_INTERVALS[skipInterval]);
};
const backTenSeconds = () => {
// @ts-ignore
player.jumpInterval(-SKIP_INTERVALS[skipInterval]);
};
const toggleBottomTools = (blockName: number) => {
toggleBottomBlock(blockName);
};
return (
<div className={styles.controls}>
<Timeline />
<div className={cn(styles.buttons, '!px-5 !pt-0')} data-is-live>
<div className="flex items-center">
{!closedLive && (
<div className={styles.buttonsLeft}>
<LiveTag isLive={livePlay} onClick={() => (livePlay ? null : jumpToLive())} />
<div className="font-semibold px-2">
<AssistDuration />
</div>
</div>
)}
</div>
{totalAssistSessions > 1 ? (
<div>
<AssistSessionsTabs session={session} />
</div>
) : null}
<div className="flex items-center h-full">
<ControlButton
onClick={() => toggleBottomTools(CONSOLE)}
active={bottomBlock === CONSOLE}
label="CONSOLE"
noIcon
labelClassName="!text-base font-semibold"
hasErrors={logRedCount > 0 || showExceptions}
containerClassName="mx-2"
/>
</div>
</div>
</div>
);
}
const ControlPlayer = observer(Controls);
export default connect(
(state: any) => {
return {
session: state.getIn(['sessions', 'current']),
totalAssistSessions: state.getIn(['liveSearch', 'total']),
closedLive:
!!state.getIn(['sessions', 'errors']) || !state.getIn(['sessions', 'current']).live,
};
},
{
fetchSessions,
toggleBottomBlock
}
)(ControlPlayer);

View file

@ -0,0 +1,31 @@
import React from 'react';
import cn from 'classnames';
import Player from './LivePlayerInst';
import SubHeader from './LivePlayerSubHeader';
import styles from 'Components/Session_/playerBlock.module.css';
interface IProps {
fullView?: boolean;
isMultiview?: boolean;
}
function LivePlayerBlock(props: IProps) {
const { fullView = false, isMultiview } = props;
const shouldShowSubHeader = !fullView && !isMultiview
return (
<div className={cn(styles.playerBlock, 'flex flex-col', 'overflow-x-hidden')} style={{ zIndex: undefined, minWidth: isMultiview ? '100%' : undefined }}>
{shouldShowSubHeader ? (
<SubHeader />
) : null}
<Player
fullView={fullView}
isMultiview={isMultiview}
/>
</div>
);
}
export default LivePlayerBlock

View file

@ -0,0 +1,122 @@
import React from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import {
assist as assistRoute,
withSiteId,
multiview,
} from 'App/routes';
import { BackLink, Icon } from 'UI';
import cn from 'classnames';
import SessionMetaList from 'Shared/SessionItem/SessionMetaList';
import UserCard from '../ReplayPlayer/EventsBlock/UserCard';
import { PlayerContext } from 'Components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore'
import stl from '../ReplayPlayer/playerBlockHeader.module.css';
import AssistActions from 'Components/Assist/components/AssistActions';
import AssistTabs from 'Components/Assist/components/AssistTabs';
const ASSIST_ROUTE = assistRoute();
// TODO props
function LivePlayerBlockHeader(props: any) {
const [hideBack, setHideBack] = React.useState(false);
const { store } = React.useContext(PlayerContext);
const { assistMultiviewStore } = useStore();
const { width, height } = store.get();
const {
session,
metaList,
closedLive = false,
siteId,
location,
history,
isMultiview,
} = props;
React.useEffect(() => {
const queryParams = new URLSearchParams(location.search);
setHideBack(queryParams.has('iframe') && queryParams.get('iframe') === 'true');
}, []);
const backHandler = () => {
history.push(withSiteId(ASSIST_ROUTE, siteId));
};
const { userId, userNumericHash, metadata, isCallActive, agentIds } = session;
let _metaList = Object.keys(metadata)
.filter((i) => metaList.includes(i))
.map((key) => {
const value = metadata[key];
return { label: key, value };
});
const openGrid = () => {
const sessionIdQuery = encodeURIComponent(assistMultiviewStore.sessions.map((s) => s?.sessionId).join(','));
return history.push(withSiteId(multiview(sessionIdQuery), siteId));
};
return (
<div className={cn(stl.header, 'flex justify-between')}>
<div className="flex w-full items-center">
{!hideBack && (
<div
className="flex items-center h-full cursor-pointer group"
onClick={() => (assistMultiviewStore.sessions.length > 1 || isMultiview ? openGrid() : backHandler())}
>
{assistMultiviewStore.sessions.length > 1 || isMultiview ? (
<>
<div className="rounded-full border group-hover:border-teal group-hover:text-teal group-hover:fill-teal p-1 mr-2">
<Icon name="close" color="inherit" size={13} />
</div>
<span className="group-hover:text-teal group-hover:fill-teal">
Close
</span>
<div className={stl.divider} />
</>
) : (
<>
{/* @ts-ignore TODO */}
<BackLink label="Back" className="h-full" />
<div className={stl.divider} />
</>
)}
</div>
)}
<UserCard className="" width={width} height={height} />
<AssistTabs userId={userId} userNumericHash={userNumericHash} />
<div className={cn('ml-auto flex items-center h-full', { hidden: closedLive })}>
{_metaList.length > 0 && (
<div className="border-l h-full flex items-center px-2">
<SessionMetaList className="" metaList={_metaList} maxLength={2} />
</div>
)}
<AssistActions userId={userId} isCallActive={isCallActive} agentIds={agentIds} />
</div>
</div>
</div>
);
}
const PlayerHeaderCont = connect(
(state: any) => {
const isAssist = window.location.pathname.includes('/assist/');
const session = state.getIn(['sessions', 'current']);
return {
isAssist,
session,
sessionPath: state.getIn(['sessions', 'sessionPath']),
siteId: state.getIn(['site', 'siteId']),
metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key),
closedLive: !!state.getIn(['sessions', 'errors']) || (isAssist && !session.live),
};
}
)(observer(LivePlayerBlockHeader));
export default withRouter(PlayerHeaderCont);

View file

@ -0,0 +1,81 @@
import React from 'react';
import { connect } from 'react-redux';
import { findDOMNode } from 'react-dom';
import cn from 'classnames';
import LiveControls from './LiveControls';
import ConsolePanel from 'Shared/DevTools/ConsolePanel';
import Overlay from './Overlay';
import stl from 'Components/Session_/Player/player.module.css';
import { PlayerContext, ILivePlayerContext } from 'App/components/Session/playerContext';
import { CONSOLE } from "Duck/components/player";
interface IProps {
closedLive: boolean;
fullView: boolean;
isMultiview?: boolean;
bottomBlock: number;
}
function Player(props: IProps) {
const {
closedLive,
fullView,
isMultiview,
bottomBlock,
} = props;
// @ts-ignore TODO
const playerContext = React.useContext<ILivePlayerContext>(PlayerContext);
const screenWrapper = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!props.closedLive || isMultiview) {
const parentElement = findDOMNode(screenWrapper.current) as HTMLDivElement | null; //TODO: good architecture
if (parentElement) {
playerContext.player.attach(parentElement);
playerContext.player.play();
}
}
}, []);
React.useEffect(() => {
playerContext.player.scale();
}, [playerContext.player]);
if (!playerContext.player) return null;
const maxWidth = '100vw';
return (
<div
className={cn(stl.playerBody, 'flex flex-1 flex-col relative')}
>
<div className="relative flex-1 overflow-hidden">
<Overlay closedLive={closedLive} />
<div className={cn(stl.screenWrapper)} ref={screenWrapper} />
</div>
{bottomBlock === CONSOLE ? (
<div style={{ maxWidth, width: '100%' }}>
<ConsolePanel />
</div>
) : null}
{!fullView && !isMultiview ? (
<LiveControls
jump={playerContext.player.jump}
/>
) : null}
</div>
);
}
export default connect(
(state: any) => {
const isAssist = window.location.pathname.includes('/assist/');
return {
sessionId: state.getIn(['sessions', 'current']).sessionId,
bottomBlock: state.getIn(['components', 'player', 'bottomBlock']),
closedLive:
!!state.getIn(['sessions', 'errors']) ||
(isAssist && !state.getIn(['sessions', 'current']).live),
};
}
)(Player);

View file

@ -0,0 +1,40 @@
import React from 'react';
import { Icon, Tooltip } from 'UI';
import copy from 'copy-to-clipboard';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
function SubHeader() {
const { store } = React.useContext(PlayerContext)
const {
location: currentLocation,
} = store.get()
const [isCopied, setCopied] = React.useState(false);
const location =
currentLocation !== undefined ? currentLocation.length > 60
? `${currentLocation.slice(0, 60)}...`
: currentLocation : undefined;
return (
<div className="w-full px-4 py-2 flex items-center border-b min-h-3">
{location && (
<div
className="flex items-center cursor-pointer color-gray-medium text-sm p-1 hover:bg-gray-light-shade rounded-md"
onClick={() => {
copy(currentLocation || '');
setCopied(true);
setTimeout(() => setCopied(false), 5000);
}}
>
<Icon size="20" name="event/link" className="mr-1" />
<Tooltip title={isCopied ? 'URL Copied to clipboard' : 'Click to copy'}>
{location}
</Tooltip>
</div>
)}
</div>
);
}
export default observer(SubHeader);

View file

@ -0,0 +1,74 @@
import React from 'react';
import {
SessionRecordingStatus,
getStatusText,
CallingState,
ConnectionStatus,
RemoteControlStatus,
} from 'Player';
import LiveStatusText from './LiveStatusText';
import Loader from 'Components/Session_/Player/Overlay/Loader';
import RequestingWindow, { WindowType } from 'App/components/Assist/RequestingWindow';
import { PlayerContext, ILivePlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
interface Props {
closedLive?: boolean,
}
function Overlay({
closedLive,
}: Props) {
// @ts-ignore ?? TODO
const { store } = React.useContext<ILivePlayerContext>(PlayerContext)
const {
messagesLoading,
cssLoading,
peerConnectionStatus,
livePlay,
calling,
remoteControl,
recordingState,
} = store.get()
const loading = messagesLoading || cssLoading
const liveStatusText = getStatusText(peerConnectionStatus)
const connectionStatus = peerConnectionStatus
const showLiveStatusText = livePlay && liveStatusText && !loading;
const showRequestWindow =
(calling === CallingState.Connecting ||
remoteControl === RemoteControlStatus.Requesting ||
recordingState === SessionRecordingStatus.Requesting);
const getRequestWindowType = () => {
if (calling === CallingState.Connecting) {
return WindowType.Call
}
if (remoteControl === RemoteControlStatus.Requesting) {
return WindowType.Control
}
if (recordingState === SessionRecordingStatus.Requesting) {
return WindowType.Record
}
return null;
}
return (
<>
{/* @ts-ignore wtf */}
{showRequestWindow ? <RequestingWindow getWindowType={getRequestWindowType} /> : null}
{showLiveStatusText && (
<LiveStatusText
connectionStatus={closedLive ? ConnectionStatus.Closed : connectionStatus}
/>
)}
{loading ? <Loader /> : null}
</>
);
}
export default observer(Overlay);

View file

@ -0,0 +1,70 @@
import React from 'react';
import ovStl from 'Components/Session_/Player/Overlay/overlay.module.css';
import { ConnectionStatus } from 'Player';
import { Loader } from 'UI';
interface Props {
connectionStatus: ConnectionStatus;
}
export default function LiveStatusText({ connectionStatus }: Props) {
const renderView = () => {
switch (connectionStatus) {
case ConnectionStatus.Closed:
return (
<div className="flex flex-col items-center text-center">
<div className="text-lg -mt-8">Session not found</div>
<div className="text-sm">The remote session doesnt exist anymore. <br/> The user may have closed the tab/browser while you were trying to establish a connection.</div>
</div>
)
case ConnectionStatus.Connecting:
return (
<div className="flex flex-col items-center">
<Loader loading={true} />
<div className="text-lg -mt-8">Connecting...</div>
<div className="text-sm">Establishing a connection with the remote session.</div>
</div>
)
case ConnectionStatus.WaitingMessages:
return (
<div className="flex flex-col items-center">
<Loader loading={true} />
<div className="text-lg -mt-8">Waiting for the session to become active...</div>
<div className="text-sm">If it's taking too much time, it could mean the user is simply inactive.</div>
</div>
)
case ConnectionStatus.Connected:
return (
<div className="flex flex-col items-center">
<div className="text-lg -mt-8">Connected</div>
</div>
)
case ConnectionStatus.Inactive:
return (
<div className="flex flex-col items-center">
<Loader loading={true} />
<div className="text-lg -mt-8">Waiting for the session to become active...</div>
<div className="text-sm">If it's taking too much time, it could mean the user is simply inactive.</div>
</div>
)
case ConnectionStatus.Disconnected:
return (
<div className="flex flex-col items-center">
<div className="text-lg -mt-8">Disconnected</div>
<div className="text-sm">The connection was lost with the remote session. The user may have simply closed the tab/browser.</div>
</div>
)
case ConnectionStatus.Error:
return (
<div className="flex flex-col items-center">
<div className="text-lg -mt-8">Error</div>
<div className="text-sm">Something wrong just happened. Try refreshing the page.</div>
</div>
)
}
}
return <div className={ovStl.overlay}>
{ renderView()}
</div>
}

View file

@ -0,0 +1 @@
export { default } from './LiveOverlay'

View file

@ -0,0 +1,166 @@
import React, { useMemo, useContext, useState, useRef } from 'react';
import { connect } from 'react-redux';
import TimeTracker from 'Components/Session_/Player/Controls/TimeTracker';
import stl from 'Components/Session_/Player/Controls/timeline.module.css';
import { setTimelinePointer, setTimelineHoverTime } from 'Duck/sessions';
import DraggableCircle from 'Components/Session_/Player/Controls/components/DraggableCircle';
import CustomDragLayer, { OnDragCallback } from 'Components/Session_/Player/Controls/components/CustomDragLayer';
import { debounce } from 'App/utils';
import TooltipContainer from 'Components/Session_/Player/Controls/components/TooltipContainer';
import { PlayerContext, ILivePlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { Duration } from 'luxon';
interface IProps {
setTimelineHoverTime: (t: number) => void
startedAt: number
tooltipVisible: boolean
}
function Timeline(props: IProps) {
// @ts-ignore
const { player, store } = useContext<ILivePlayerContext>(PlayerContext)
const [wasPlaying, setWasPlaying] = useState(false)
const {
playing,
time,
ready,
endTime,
liveTimeTravel,
} = store.get()
const timelineRef = useRef<HTMLDivElement>(null)
const progressRef = useRef<HTMLDivElement>(null)
const scale = 100 / endTime;
const debouncedJump = useMemo(() => debounce(player.jump, 500), [])
const debouncedTooltipChange = useMemo(() => debounce(props.setTimelineHoverTime, 50), [])
const onDragEnd = () => {
if (!liveTimeTravel) return;
if (wasPlaying) {
player.togglePlay();
}
};
const onDrag: OnDragCallback = (offset: { x: number }) => {
if ((!liveTimeTravel) || !progressRef.current) return;
const p = (offset.x) / progressRef.current.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
debouncedJump(time);
hideTimeTooltip();
if (playing) {
setWasPlaying(true)
player.pause();
}
};
const getLiveTime = (e: React.MouseEvent) => {
const duration = new Date().getTime() - props.startedAt;
// @ts-ignore type mismatch from react?
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const time = Math.max(Math.round(p * duration), 0);
return [time, duration];
};
const showTimeTooltip = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target !== progressRef.current && e.target !== timelineRef.current) {
return props.tooltipVisible && hideTimeTooltip();
}
const [time, duration] = getLiveTime(e);
const timeLineTooltip = {
time: Duration.fromMillis(duration - time).toFormat(`-mm:ss`),
offset: e.nativeEvent.offsetX,
isVisible: true,
};
debouncedTooltipChange(timeLineTooltip);
}
const hideTimeTooltip = () => {
const timeLineTooltip = { isVisible: false };
debouncedTooltipChange(timeLineTooltip);
};
const seekProgress = (e: React.MouseEvent<HTMLDivElement>) => {
const time = getTime(e);
player.jump(time);
hideTimeTooltip();
};
const loadAndSeek = async (e: React.MouseEvent<HTMLDivElement>) => {
e.persist();
await player.toggleTimetravel();
setTimeout(() => {
seekProgress(e);
});
};
const jumpToTime = (e: React.MouseEvent<HTMLDivElement>) => {
if (!liveTimeTravel) {
void loadAndSeek(e);
} else {
seekProgress(e);
}
};
const getTime = (e: React.MouseEvent<HTMLDivElement>, customEndTime?: number) => {
// @ts-ignore type mismatch from react?
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const targetTime = customEndTime || endTime;
return Math.max(Math.round(p * targetTime), 0);
};
return (
<div
className="flex items-center absolute w-full"
style={{
top: '-4px',
zIndex: 100,
maxWidth: 'calc(100% - 1rem)',
left: '0.5rem',
}}
>
<div
className={stl.progress}
onClick={ready ? jumpToTime : undefined }
ref={progressRef}
role="button"
onMouseMoveCapture={showTimeTooltip}
onMouseEnter={showTimeTooltip}
onMouseLeave={hideTimeTooltip}
>
<TooltipContainer live />
<DraggableCircle
left={time * scale}
onDrop={onDragEnd}
live
/>
<CustomDragLayer
onDrag={onDrag}
minX={0}
maxX={progressRef.current ? progressRef.current.offsetWidth : 0}
/>
<TimeTracker scale={scale} live left={time * scale} />
<div className={stl.timeline} ref={timelineRef} />
</div>
</div>
)
}
export default connect(
(state: any) => ({
startedAt: state.getIn(['sessions', 'current']).startedAt || 0,
tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible']),
}),
{ setTimelinePointer, setTimelineHoverTime }
)(observer(Timeline))

View file

@ -0,0 +1,174 @@
import React from 'react';
import copy from 'copy-to-clipboard';
import cn from 'classnames';
import { Icon, TextEllipsis } from 'UI';
import { TYPES } from 'Types/session/event';
import { prorata } from 'App/utils';
import withOverlay from 'Components/hocs/withOverlay';
import LoadInfo from './LoadInfo';
import cls from './event.module.css';
import { numberWithCommas } from 'App/utils';
@withOverlay()
export default class Event extends React.PureComponent {
state = {
menuOpen: false,
}
componentDidMount() {
this.wrapper.addEventListener('contextmenu', this.onContextMenu);
}
onContextMenu = (e) => {
e.preventDefault();
this.setState({ menuOpen: true });
}
onMouseLeave = () => this.setState({ menuOpen: false })
copyHandler = (e) => {
e.stopPropagation();
//const ctrlOrCommandPressed = e.ctrlKey || e.metaKey;
//if (ctrlOrCommandPressed && e.keyCode === 67) {
const { event } = this.props;
copy(event.getIn([ 'target', 'path' ]) || event.url || '');
this.setState({ menuOpen: false });
}
toggleInfo = (e) => {
e.stopPropagation();
this.props.toggleInfo();
}
// eslint-disable-next-line complexity
renderBody = () => {
const { event } = this.props;
let title = event.type;
let body;
switch (event.type) {
case TYPES.LOCATION:
title = 'Visited';
body = event.url;
break;
case TYPES.CLICK:
title = 'Clicked';
body = event.label;
break;
case TYPES.INPUT:
title = 'Input';
body = event.value;
break;
case TYPES.CLICKRAGE:
title = `${ event.count } Clicks`;
body = event.label;
break;
case TYPES.IOS_VIEW:
title = 'View';
body = event.name;
break;
}
const isLocation = event.type === TYPES.LOCATION;
const isClickrage = event.type === TYPES.CLICKRAGE;
return (
<div className={ cn(cls.main, 'flex flex-col w-full') } >
<div className="flex items-center w-full">
{ event.type && <Icon name={`event/${event.type.toLowerCase()}`} size="16" color={isClickrage? 'red' : 'gray-dark' } /> }
<div className="ml-3 w-full">
<div className="flex w-full items-first justify-between">
<div className="flex items-center w-full" style={{ minWidth: '0'}}>
<span className={cls.title}>{ title }</span>
{/* { body && !isLocation && <div className={ cls.data }>{ body }</div> } */}
{ body && !isLocation &&
<TextEllipsis maxWidth="60%" className="w-full ml-2 text-sm color-gray-medium" text={body} />
}
</div>
{ isLocation && event.speedIndex != null &&
<div className="color-gray-medium flex font-medium items-center leading-none justify-end">
<div className="font-size-10 pr-2">{"Speed Index"}</div>
<div>{ numberWithCommas(event.speedIndex || 0) }</div>
</div>
}
</div>
{ event.target && event.target.label &&
<div className={ cls.badge } >{ event.target.label }</div>
}
</div>
</div>
{ isLocation &&
<div className="mt-1">
<span className="text-sm font-normal color-gray-medium">{ body }</span>
</div>
}
</div>
);
};
render() {
const {
event,
selected,
isCurrent,
onClick,
showSelection,
onCheckboxClick,
showLoadInfo,
toggleLoadInfo,
isRed,
extended,
highlight = false,
presentInSearch = false,
isLastInGroup,
whiteBg,
} = this.props;
const { menuOpen } = this.state;
return (
<div
ref={ ref => { this.wrapper = ref } }
onMouseLeave={ this.onMouseLeave }
data-openreplay-label="Event"
data-type={event.type}
className={ cn(cls.event, {
[ cls.menuClosed ]: !menuOpen,
[ cls.highlighted ]: showSelection ? selected : isCurrent,
[ cls.selected ]: selected,
[ cls.showSelection ]: showSelection,
[ cls.red ]: isRed,
[ cls.clickType ]: event.type === TYPES.CLICK,
[ cls.inputType ]: event.type === TYPES.INPUT,
[ cls.clickrageType ]: event.type === TYPES.CLICKRAGE,
[ cls.highlight ] : presentInSearch,
[ cls.lastInGroup ]: whiteBg,
}) }
onClick={ onClick }
>
{ menuOpen &&
<button onClick={ this.copyHandler } className={ cls.contextMenu }>
{ event.target ? 'Copy CSS' : 'Copy URL' }
</button>
}
<div className={ cls.topBlock }>
<div className={ cls.firstLine }>
{ this.renderBody() }
</div>
{/* { event.type === TYPES.LOCATION &&
<div className="text-sm font-normal color-gray-medium">{event.url}</div>
} */}
</div>
{ event.type === TYPES.LOCATION && (event.fcpTime || event.visuallyComplete || event.timeToInteractive) &&
<LoadInfo
showInfo={ showLoadInfo }
onClick={ toggleLoadInfo }
event={ event }
prorata={ prorata({
parts: 100,
elements: { a: event.fcpTime, b: event.visuallyComplete, c: event.timeToInteractive },
startDivisorFn: elements => elements / 1.2,
// eslint-disable-next-line no-mixed-operators
divisorFn: (elements, parts) => elements / (2 * parts + 1),
}) }
/>
}
</div>
);
}
}

View file

@ -0,0 +1,131 @@
import React from 'react';
import cn from 'classnames';
import { connect } from 'react-redux'
import { TextEllipsis } from 'UI';
import withToggle from 'HOCs/withToggle';
import { TYPES } from 'Types/session/event';
import Event from './Event'
import stl from './eventGroupWrapper.module.css';
import NoteEvent from './NoteEvent';
import { setEditNoteTooltip } from 'Duck/sessions';;
// TODO: incapsulate toggler in LocationEvent
@withToggle('showLoadInfo', 'toggleLoadInfo')
@connect(
(state) => ({
members: state.getIn(['members', 'list']),
currentUserId: state.getIn(['user', 'account', 'id']),
}),
{ setEditNoteTooltip }
)
class EventGroupWrapper extends React.Component {
toggleLoadInfo = (e) => {
e.stopPropagation();
this.props.toggleLoadInfo();
};
componentDidUpdate(prevProps) {
if (
prevProps.showLoadInfo !== this.props.showLoadInfo ||
prevProps.query !== this.props.query ||
prevProps.event.timestamp !== this.props.event.timestamp ||
prevProps.isNote !== this.props.isNote
) {
this.props.mesureHeight();
}
}
componentDidMount() {
this.props.toggleLoadInfo(this.props.isFirst);
this.props.mesureHeight();
}
onEventClick = (e) => this.props.onEventClick(e, this.props.event);
onCheckboxClick = (e) => this.props.onCheckboxClick(e, this.props.event);
render() {
const {
event,
isLastEvent,
isLastInGroup,
isSelected,
isCurrent,
isEditing,
showSelection,
showLoadInfo,
isFirst,
presentInSearch,
isNote,
filterOutNote,
} = this.props;
const isLocation = event.type === TYPES.LOCATION;
const whiteBg =
(isLastInGroup && event.type !== TYPES.LOCATION) ||
(!isLastEvent && event.type !== TYPES.LOCATION);
const safeRef = String(event.referrer || '');
return (
<div
className={cn(
stl.container,
'!py-1',
{
[stl.last]: isLastInGroup,
[stl.first]: event.type === TYPES.LOCATION,
[stl.dashAfter]: isLastInGroup && !isLastEvent,
},
isLastInGroup && '!pb-2',
event.type === TYPES.LOCATION && '!pt-2 !pb-2'
)}
>
{isFirst && isLocation && event.referrer && (
<div className={stl.referrer}>
<TextEllipsis>
Referrer: <span className={stl.url}>{safeRef}</span>
</TextEllipsis>
</div>
)}
{isNote ? (
<NoteEvent
userEmail={this.props.members.find((m) => m.id === event.userId)?.email || event.userId}
note={event}
filterOutNote={filterOutNote}
onEdit={this.props.setEditNoteTooltip}
noEdit={this.props.currentUserId !== event.userId}
/>
) : isLocation ? (
<Event
extended={isFirst}
key={event.key}
event={event}
onClick={this.onEventClick}
selected={isSelected}
showLoadInfo={showLoadInfo}
toggleLoadInfo={this.toggleLoadInfo}
isCurrent={isCurrent}
presentInSearch={presentInSearch}
isLastInGroup={isLastInGroup}
whiteBg={whiteBg}
/>
) : (
<Event
key={event.key}
event={event}
onClick={this.onEventClick}
onCheckboxClick={this.onCheckboxClick}
selected={isSelected}
isCurrent={isCurrent}
showSelection={showSelection}
overlayed={isEditing}
presentInSearch={presentInSearch}
isLastInGroup={isLastInGroup}
whiteBg={whiteBg}
/>
)}
</div>
);
}
}
export default EventGroupWrapper

View file

@ -0,0 +1,43 @@
import React from 'react'
import { Input, Icon } from 'UI'
import { PlayerContext } from 'App/components/Session/playerContext';
function EventSearch(props) {
const { player } = React.useContext(PlayerContext)
const { onChange, value, header, setActiveTab } = props;
const toggleEvents = () => player.toggleEvents()
return (
<div className="flex items-center w-full relative">
<div className="flex flex-1 flex-col">
<div className='flex flex-center justify-between'>
<span>{header}</span>
<div
onClick={() => { setActiveTab(''); toggleEvents(); }}
className=" flex items-center justify-center bg-white cursor-pointer"
>
<Icon name="close" size="18" />
</div>
</div>
<div className="flex items-center mt-2">
<Input
autoFocus
type="text"
placeholder="Filter by Event Type, URL or Keyword"
className="inset-0 w-full"
name="query"
value={value}
onChange={onChange}
wrapperClassName="w-full"
style={{ height: '32px' }}
autoComplete="off chromebugfix"
/>
</div>
</div>
</div>
)
}
export default EventSearch

View file

@ -0,0 +1 @@
export { default } from './EventSearch'

View file

@ -0,0 +1,185 @@
import React from 'react';
import { connect } from 'react-redux';
import cn from 'classnames';
import { Icon } from 'UI';
import { List, AutoSizer, CellMeasurer } from "react-virtualized";
import { TYPES } from 'Types/session/event';
import { setEventFilter, filterOutNote } from 'Duck/sessions';
import EventGroupWrapper from './EventGroupWrapper';
import styles from './eventsBlock.module.css';
import EventSearch from './EventSearch/EventSearch';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { RootStore } from 'App/duck'
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache'
import { InjectedEvent } from 'Types/session/event'
import Session from 'Types/session'
interface IProps {
setEventFilter: (filter: { query: string }) => void
filteredEvents: InjectedEvent[]
setActiveTab: (tab?: string) => void
query: string
session: Session
filterOutNote: (id: string) => void
eventsIndex: number[]
}
function EventsBlock(props: IProps) {
const [mouseOver, setMouseOver] = React.useState(true)
const scroller = React.useRef<List>(null)
const cache = useCellMeasurerCache(undefined, {
fixedWidth: true,
defaultHeight: 300
});
const { store, player } = React.useContext(PlayerContext)
const { eventListNow, playing } = store.get()
const { session: { events, notesWithEvents }, filteredEvents,
eventsIndex,
filterOutNote,
query,
setActiveTab,
} = props
const currentTimeEventIndex = eventListNow.length > 0 ? eventListNow.length - 1 : 0
const usedEvents = filteredEvents || notesWithEvents
const write = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
props.setEventFilter({ query: value })
setTimeout(() => {
if (!scroller.current) return;
scroller.current.scrollToRow(0);
}, 100)
}
const clearSearch = () => {
props.setEventFilter({ query: '' })
if (scroller.current) {
scroller.current.forceUpdateGrid();
}
setTimeout(() => {
if (!scroller.current) return;
scroller.current.scrollToRow(0);
}, 100)
}
React.useEffect(() => {
return () => {
clearSearch()
}
}, [])
React.useEffect(() => {
if (scroller.current) {
scroller.current.forceUpdateGrid();
if (!mouseOver) {
scroller.current.scrollToRow(currentTimeEventIndex);
}
}
}, [currentTimeEventIndex])
const onEventClick = (_: React.MouseEvent, event: { time: number }) => player.jump(event.time)
const onMouseOver = () => setMouseOver(true)
const onMouseLeave = () => setMouseOver(false)
const renderGroup = ({ index, key, style, parent }: { index: number; key: string; style: React.CSSProperties; parent: any }) => {
const isLastEvent = index === usedEvents.length - 1;
const isLastInGroup = isLastEvent || usedEvents[index + 1]?.type === TYPES.LOCATION;
const event = usedEvents[index];
const isNote = 'noteId' in event
const isCurrent = index === currentTimeEventIndex;
const heightBug = index === 0 && event?.type === TYPES.LOCATION && 'referrer' in event ? { top: 2 } : {}
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
rowIndex={index}
>
{({measure, registerChild}) => (
<div style={{ ...style, ...heightBug }} ref={registerChild}>
<EventGroupWrapper
query={query}
presentInSearch={eventsIndex.includes(index)}
isFirst={index==0}
mesureHeight={measure}
onEventClick={ onEventClick }
event={ event }
isLastEvent={ isLastEvent }
isLastInGroup={ isLastInGroup }
isCurrent={ isCurrent }
showSelection={ !playing }
isNote={isNote}
filterOutNote={filterOutNote}
/>
</div>
)}
</CellMeasurer>
);
}
const isEmptySearch = query && (usedEvents.length === 0 || !usedEvents)
return (
<>
<div className={ cn(styles.header, 'p-4') }>
<div className={ cn(styles.hAndProgress, 'mt-3') }>
<EventSearch
onChange={write}
setActiveTab={setActiveTab}
value={query}
header={
<div className="text-xl">User Steps <span className="color-gray-medium">{ events.length }</span></div>
}
/>
</div>
</div>
<div
className={ cn("flex-1 px-4 pb-4", styles.eventsList) }
id="eventList"
data-openreplay-masked
onMouseOver={ onMouseOver }
onMouseLeave={ onMouseLeave }
>
{isEmptySearch && (
<div className='flex items-center'>
<Icon name="binoculars" size={18} />
<span className='ml-2'>No Matching Results</span>
</div>
)}
<AutoSizer disableWidth>
{({ height }) => (
<List
ref={scroller}
className={ styles.eventsList }
height={height + 10}
width={248}
overscanRowCount={6}
itemSize={230}
rowCount={usedEvents.length}
deferredMeasurementCache={cache}
rowHeight={cache.rowHeight}
rowRenderer={renderGroup}
scrollToAlignment="start"
/>
)}
</AutoSizer>
</div>
</>
);
}
export default connect((state: RootStore) => ({
session: state.getIn([ 'sessions', 'current' ]),
filteredEvents: state.getIn([ 'sessions', 'filteredEvents' ]),
query: state.getIn(['sessions', 'eventsQuery']),
eventsIndex: state.getIn([ 'sessions', 'eventsIndex' ]),
}), {
setEventFilter,
filterOutNote
})(observer(EventsBlock))

View file

@ -0,0 +1,40 @@
import React from 'react';
import styles from './loadInfo.module.css';
import { numberWithCommas } from 'App/utils'
const LoadInfo = ({ showInfo = false, onClick, event: { fcpTime, visuallyComplete, timeToInteractive }, prorata: { a, b, c } }) => (
<div>
<div className={ styles.bar } onClick={ onClick }>
{ typeof fcpTime === 'number' && <div style={ { width: `${ a }%` } } /> }
{ typeof visuallyComplete === 'number' && <div style={ { width: `${ b }%` } } /> }
{ typeof timeToInteractive === 'number' && <div style={ { width: `${ c }%` } } /> }
</div>
<div className={ styles.bottomBlock } data-hidden={ !showInfo }>
{ typeof fcpTime === 'number' &&
<div className={ styles.wrapper }>
<div className={ styles.lines } />
<div className={ styles.label } >{ 'Time to Render' }</div>
<div className={ styles.value }>{ `${ numberWithCommas(fcpTime || 0) }ms` }</div>
</div>
}
{ typeof visuallyComplete === 'number' &&
<div className={ styles.wrapper }>
<div className={ styles.lines } />
<div className={ styles.label } >{ 'Visually Complete' }</div>
<div className={ styles.value }>{ `${ numberWithCommas(visuallyComplete || 0) }ms` }</div>
</div>
}
{ typeof timeToInteractive === 'number' &&
<div className={ styles.wrapper }>
<div className={ styles.lines } />
<div className={ styles.label } >{ 'Time To Interactive' }</div>
<div className={ styles.value }>{ `${ numberWithCommas(timeToInteractive || 0) }ms` }</div>
</div>
}
</div>
</div>
);
LoadInfo.displayName = 'LoadInfo';
export default LoadInfo;

View file

@ -0,0 +1,25 @@
import React from 'react';
import { connect } from 'react-redux';
import MetadataItem from './MetadataItem';
export default connect(state => ({
metadata: state.getIn([ 'sessions', 'current' ]).metadata,
}))(function Metadata ({ metadata }) {
const metaLenth = Object.keys(metadata).length;
if (metaLenth === 0) {
return (
(<span className="text-sm color-gray-medium">Check <a href="https://docs.openreplay.com/installation/metadata" target="_blank" className="link">how to use Metadata</a> if you havent yet done so.</span>)
)
}
return (
<div>
{ Object.keys(metadata).map((key) => {
// const key = Object.keys(i)[0]
const value = metadata[key]
return <MetadataItem item={ { value, key } } key={ key } />
}) }
</div>
);
});

View file

@ -0,0 +1,75 @@
import React from 'react';
import { List } from 'immutable';
import cn from 'classnames';
import { withRequest, withToggle } from 'HOCs';
import { Button, Icon, SlideModal, TextEllipsis } from 'UI';
import stl from './metadataItem.module.css';
import SessionList from './SessionList';
@withToggle()
@withRequest({
initialData: List(),
endpoint: '/metadata/session_search',
dataWrapper: data => Object.values(data),
dataName: 'similarSessions',
})
export default class extends React.PureComponent {
state = {
requested: false,
}
switchOpen = () => {
const {
item: {
key, value
},
request,
switchOpen,
} = this.props;
const { requested } = this.state;
if (!requested) {
this.setState({ requested: true });
request({ key, value });
}
switchOpen();
}
render() {
const {
item,
similarSessions,
open,
loading,
} = this.props;
return (
<div>
<SlideModal
title={ <div className={ stl.searchResultsHeader }>{ `All Sessions Matching - ` } <span>{ item.key + ' - ' + item.value }</span> </div> }
isDisplayed={ open }
content={ open && <SessionList similarSessions={ similarSessions } loading={ loading } /> }
onClose={ open ? this.switchOpen : () => null }
/>
<div className={ cn("flex justify-between items-center p-3 capitalize", stl.field) } >
<div>
<div className={ stl.key }>{ item.key }</div>
<TextEllipsis
maxWidth="210px"
popupProps={ { disabled: item.value && item.value.length < 30 } }
>
{ item.value }
</TextEllipsis>
</div>
<Button
onClick={ this.switchOpen }
variant="text"
className={ stl.searchButton }
id="metadata-item"
>
<Icon name="search" size="16" color="teal" />
</Button>
</div>
</div>
);
}
}

View file

@ -0,0 +1,40 @@
import { BrowserIcon, OsIcon, Icon, CountryFlag, Link } from 'UI';
import { deviceTypeIcon } from 'App/iconNames';
import { session as sessionRoute } from 'App/routes';
import { formatTimeOrDate } from 'App/date';
const SessionLine = ({ session: {
userBrowser,
userOs,
userCountry,
siteId,
sessionId,
viewed,
userDeviceType,
startedAt
} }) => (
<div className="flex justify-between items-center" style={{ padding: '5px 20px' }}>
<div className="color-gray-medium font-size-10" >
<CountryFlag country={ userCountry } className="mr-4" />
{ formatTimeOrDate(startedAt) }
</div>
<div className="flex">
<BrowserIcon browser={ userBrowser } className="mr-4" />
<OsIcon os={ userOs } size="20" className="mr-4" />
<Icon name={ deviceTypeIcon(userDeviceType) } size="20" className="mr-4" />
</div>
<Link to={ sessionRoute(sessionId) } siteId={ siteId } >
<Icon
name={ viewed ? 'play-fill' : 'play-circle-light' }
size="20"
color="teal"
/>
</Link>
</div>
);
SessionLine.displayName = "SessionLine";
export default SessionLine;

View file

@ -0,0 +1,60 @@
import React from 'react';
import { connect } from 'react-redux';
import { NoContent, Icon, Loader } from 'UI';
import Session from 'Types/session';
import SessionItem from 'Shared/SessionItem';
import stl from './sessionList.module.css';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
@connect((state) => ({
currentSessionId: state.getIn(['sessions', 'current']).sessionId,
}))
class SessionList extends React.PureComponent {
render() {
const { similarSessions, loading, currentSessionId } = this.props;
const similarSessionWithoutCurrent = similarSessions
.map(({ sessions, ...rest }) => {
return {
...rest,
sessions: sessions.map(s => new Session(s)).filter(({ sessionId }) => sessionId !== currentSessionId),
};
})
.filter((site) => site.sessions.length > 0);
return (
<Loader loading={loading}>
<NoContent
show={!loading && (similarSessionWithoutCurrent.length === 0 || similarSessionWithoutCurrent.size === 0)}
title={
<div className="flex items-center justify-center flex-col">
<AnimatedSVG name={ICONS.NO_SESSIONS} size={170} />
<div className="mt-2" />
<div className="text-center text-gray-600">No sessions found.</div>
</div>
}
>
<div className={stl.sessionList}>
{similarSessionWithoutCurrent.map((site) => (
<div className={stl.siteWrapper} key={site.host}>
<div className={stl.siteHeader}>
<Icon name="window" size="14" color="gray-medium" marginRight="10" />
<span>{site.name}</span>
</div>
<div className="bg-white p-3 rounded border">
{site.sessions.map((session) => (
<div className="border-b last:border-none">
<SessionItem key={session.sessionId} session={session} />
</div>
))}
</div>
</div>
))}
</div>
</NoContent>
</Loader>
);
}
}
export default SessionList;

View file

@ -0,0 +1 @@
export { default } from './Metadata';

View file

@ -0,0 +1,51 @@
@import 'mixins.css';
/* .wrapper {
position: relative;
} */
.modal {
/* width: 288px; */
/* position: absolute; */
/* top: 50px; */
/* right: 0; */
/* background-color: white; */
/* border-radius: 3px; */
/* z-index: 99; */
/* padding: 10px; */
/* min-height: 90px; */
max-height: 300px;
overflow: auto;
/* @mixin shadow; */
/* border: solid thin $gray-light; */
/* & .tooltipArrow {
width: 50px;
height: 25px;
position: absolute;
bottom: 100%;
right: 0;
transform: translateX(-50%);
overflow: hidden;
&::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
background: white;
transform: translateX(-50%) translateY(50%) rotate(45deg);
bottom: 0;
left: 50%;
box-shadow: 2px 2px 6px 0px rgba(0,0,0,0.6);
}
} */
}
.header {
font-size: 18px;
font-weight: 500;
/* padding: 10px 20px; */
border-bottom: solid thin $gray-light;
}

View file

@ -0,0 +1,37 @@
.field {
&:not(:last-child) {
border-bottom: solid thin $gray-light-shade;
}
/* padding: 10px 20px; */
}
.key {
color: $gray-medium;
font-weight: 500;
}
.searchResultsHeader {
& span {
padding: 4px 8px;
font-size: 18px;
background-color: $gray-lightest;
border: solid thin $gray-light;
margin-left: 10px;
border-radius: 3px;
}
}
.searchButton {
border-radius: 3px;
height: 30px !important;
width: 30px !important;
display: flex !important;
align-items: center !important;
padding: 0 !important;
justify-content: center !important;
&:hover {
background-color: $gray-lightest !important;
}
}

View file

@ -0,0 +1,26 @@
.sessionList {
padding: 0 20px;
background-color: #f6f6f6;
min-height: calc(100vh - 59px);
}
.siteWrapper {
padding-top: 10px;
margin-bottom: 10px;
}
.siteHeader {
display: flex;
align-items: center;
margin-bottom: 15px;
font-weight: 400;
font-size: 14px;
background-color: white;
border-top: solid thin $gray-lightest;
margin: -15px;
margin-top: -10px;
margin-bottom: 20px;
padding: 10px;
}

View file

@ -0,0 +1,128 @@
import React from 'react';
import { Icon } from 'UI';
import { tagProps, Note } from 'App/services/NotesService';
import { formatTimeOrDate } from 'App/date';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { ItemMenu } from 'UI';
import copy from 'copy-to-clipboard';
import { toast } from 'react-toastify';
import { session } from 'App/routes';
import { confirm } from 'UI';
import { TeamBadge } from 'Shared/SessionListContainer/components/Notes';
interface Props {
note: Note;
noEdit: boolean;
userEmail: string;
filterOutNote: (id: number) => void;
onEdit: (noteTooltipObj: Record<string, any>) => void;
}
function NoteEvent(props: Props) {
const { settingsStore, notesStore } = useStore();
const { timezone } = settingsStore.sessionSettings;
const onEdit = () => {
props.onEdit({
isVisible: true,
isEdit: true,
time: props.note.timestamp,
note: {
timestamp: props.note.timestamp,
tag: props.note.tag,
isPublic: props.note.isPublic,
message: props.note.message,
sessionId: props.note.sessionId,
noteId: props.note.noteId,
},
});
};
const onCopy = () => {
copy(
`${window.location.origin}/${window.location.pathname.split('/')[1]}${session(
props.note.sessionId
)}${props.note.timestamp > 0 ? `?jumpto=${props.note.timestamp}&note=${props.note.noteId}` : `?note=${props.note.noteId}`}`
);
toast.success('Note URL copied to clipboard');
};
const onDelete = async () => {
if (
await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to delete this note?`,
})
) {
notesStore.deleteNote(props.note.noteId).then((r) => {
props.filterOutNote(props.note.noteId);
toast.success('Note deleted');
});
}
};
const menuItems = [
{ icon: 'pencil', text: 'Edit', onClick: onEdit, disabled: props.noEdit },
{ icon: 'link-45deg', text: 'Copy URL', onClick: onCopy },
{ icon: 'trash', text: 'Delete', onClick: onDelete },
];
return (
<div
className="flex items-start flex-col p-2 border rounded"
style={{ background: '#FFFEF5' }}
>
<div className="flex items-center w-full relative">
<div className="p-3 bg-gray-light rounded-full">
<Icon name="quotes" color="main" />
</div>
<div className="ml-2">
<div
className="text-base"
style={{
maxWidth: 150,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{props.userEmail}, {props.userEmail}
</div>
<div className="text-disabled-text text-sm">
{formatTimeOrDate(props.note.createdAt as unknown as number, timezone)}
</div>
</div>
<div className="cursor-pointer absolute" style={{ right: -5 }}>
<ItemMenu bold items={menuItems} />
</div>
</div>
<div
className="text-base capitalize-first my-3 overflow-y-scroll overflow-x-hidden"
style={{ maxHeight: 200, maxWidth: 220 }}
>
{props.note.message}
</div>
<div>
<div className="flex items-center gap-2 flex-wrap w-full">
{props.note.tag ? (
<div
key={props.note.tag}
style={{
// @ts-ignore
background: tagProps[props.note.tag],
userSelect: 'none',
padding: '1px 6px',
}}
className="rounded-full text-white text-xs select-none w-fit"
>
{props.note.tag}
</div>
) : null}
{!props.note.isPublic ? null : <TeamBadge />}
</div>
</div>
</div>
);
}
export default observer(NoteEvent);

View file

@ -0,0 +1,123 @@
import React from 'react';
import { connect } from 'react-redux';
import { List } from 'immutable';
import { countries } from 'App/constants';
import { useStore } from 'App/mstore';
import { browserIcon, osIcon, deviceTypeIcon } from 'App/iconNames';
import { formatTimeOrDate } from 'App/date';
import { Avatar, TextEllipsis, CountryFlag, Icon, Tooltip, Popover } from 'UI';
import cn from 'classnames';
import { withRequest } from 'HOCs';
import SessionInfoItem from 'Components/Session_/SessionInfoItem';
import { useModal } from 'App/components/Modal';
import UserSessionsModal from 'Shared/UserSessionsModal';
function UserCard({ className, request, session, width, height, similarSessions, loading }) {
const { settingsStore } = useStore();
const { timezone } = settingsStore.sessionSettings;
const {
userBrowser,
userDevice,
userCountry,
userBrowserVersion,
userOs,
userOsVersion,
startedAt,
userId,
userAnonymousId,
userNumericHash,
userDisplayName,
userDeviceType,
revId,
} = session;
const hasUserDetails = !!userId || !!userAnonymousId;
const getDimension = (width, height) => {
return width && height ? (
<div className="flex items-center">
{width || 'x'} <Icon name="close" size="12" className="mx-1" /> {height || 'x'}
</div>
) : (
<span className="">Resolution N/A</span>
);
};
const avatarbgSize = '38px';
return (
<div className={cn('bg-white flex items-center w-full', className)}>
<div className="flex items-center">
<Avatar iconSize="23" width={avatarbgSize} height={avatarbgSize} seed={userNumericHash} />
<div className="ml-3 overflow-hidden leading-tight">
<TextEllipsis
noHint
className={cn('font-medium', { 'color-teal cursor-pointer': hasUserDetails })}
// onClick={hasUserDetails ? showSimilarSessions : undefined}
>
<UserName name={userDisplayName} userId={userId} hash={userNumericHash} />
</TextEllipsis>
<div className="text-sm color-gray-medium flex items-center">
<span style={{ whiteSpace: 'nowrap' }}>
<Tooltip
title={`${formatTimeOrDate(startedAt, timezone, true)} ${timezone.label}`}
className="w-fit !block"
>
{formatTimeOrDate(startedAt, timezone)}
</Tooltip>
</span>
<span className="mx-1 font-bold text-xl">&#183;</span>
<span>{countries[userCountry]}</span>
<span className="mx-1 font-bold text-xl">&#183;</span>
<span className="capitalize">
{userBrowser}, {userOs}, {userDevice}
</span>
<span className="mx-1 font-bold text-xl">&#183;</span>
<Popover
render={() => (
<div className="text-left bg-white">
<SessionInfoItem
comp={<CountryFlag country={userCountry} />}
label={countries[userCountry]}
value={<span style={{ whiteSpace: 'nowrap' }}>{formatTimeOrDate(startedAt)}</span>}
/>
<SessionInfoItem icon={browserIcon(userBrowser)} label={userBrowser} value={`v${userBrowserVersion}`} />
<SessionInfoItem icon={osIcon(userOs)} label={userOs} value={userOsVersion} />
<SessionInfoItem
icon={deviceTypeIcon(userDeviceType)}
label={userDeviceType}
value={getDimension(width, height)}
isLast={!revId}
/>
{revId && <SessionInfoItem icon="info" label="Rev ID:" value={revId} isLast />}
</div>
)}
>
<span className="link">More</span>
</Popover>
</div>
</div>
</div>
</div>
);
}
const component = React.memo(connect((state) => ({ session: state.getIn(['sessions', 'current']) }))(UserCard));
export default withRequest({
initialData: List(),
endpoint: '/metadata/session_search',
dataWrapper: (data) => Object.values(data),
dataName: 'similarSessions',
})(component);
// inner component
function UserName({ name, userId, hash }) {
const { showModal } = useModal();
const onClick = () => {
showModal(<UserSessionsModal userId={userId} hash={hash} name={name} />, { right: true });
};
return <div onClick={userId ? onClick : () => {}}>{name}</div>;
}

View file

@ -0,0 +1 @@
export { default } from './UserCard';

View file

@ -0,0 +1,165 @@
.contextMenu {
position: absolute;
top: 27px;
right: 15px;
padding: 2px 3px;
background: $white;
border: 1px solid $gray-light;
border-radius: 3px;
cursor: pointer;
color: $gray-medium;
font-size: 12px;
z-index: 2;
}
.event {
position: relative;
background: #f6f6f6;
border-radius: 3px;
user-select: none;
/* box-shadow: 0px 1px 3px 0 $gray-light; */
transition: all 0.2s;
cursor: pointer;
border: 1px solid transparent;
&:hover {
background-color: $active-blue;
border: 1px solid $active-blue-border;
}
& .title {
font-size: 13px;
}
& .topBlock {
min-height: 30px;
position: relative;
padding: 8px 10px;
}
& .checkbox {
position: absolute;
left: 10px;
top: 8px;
bottom: 0;
/* margin: auto; */
display: none;
/* align-items: center; */
}
&.menuClosed:hover {
& .edit {
opacity: 1;
transition: all 0.2s;
}
}
&.menuClosed.showSelection {
&:hover, &.selected {
background-color: #EFFCFB;
& .checkbox {
display: flex;
}
& .icon {
opacity: 0;
}
}
}
&.highlighted {
transition: all 0.2s;
box-shadow: 0px 2px 10px 0 $gray-light;
border: 1px solid $active-blue-border;
/* background-color: red; */
}
&.red {
border-color: $red;
}
}
.firstLine {
display: flex;
justify-content: space-between;
align-items: center;
}
.main {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
align-items: flex-start;
}
.type {
color: $gray-dark;
font-size: 12px;
text-transform: capitalize;
font-weight: bold;
}
.data {
margin-left: 5px;
color: $gray-medium;
font-size: 12px;
max-width: 100%;
/* overflow: hidden; */
/* text-overflow: ellipsis; */
}
.badge {
display: inline-block;
padding: 0;
border-radius: 3px;
font-size: 9px;
/* margin-left: 28px; */
max-width: 170px;
word-wrap: break-word;
line-height: normal;
color: #999;
text-transform: none;
}
.icon {
margin-right: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
& i {
width: 18px;
height: 18px;
}
}
.clickType, .inputType {
/* border: 1px solid $gray-light; */
background-color: $gray-lightest;
cursor: pointer;
}
.clickrageType {
background-color: #FFF3F3;
border: 1px solid #CC0000;
box-shadow:
/* The top layer shadow */
/* 0 1px 1px rgba(0,0,0,0.15), */
/* The second layer */
2px 2px 1px 1px white,
/* The second layer shadow */
2px 2px 0px 1px rgba(0,0,0,0.4);
/* Padding for demo purposes */
/* padding: 12px; */
}
.highlight {
border: solid thin red;
}
.lastInGroup {
background: white;
box-shadow: 0px 1px 1px 0px rgb(0 0 0 / 18%);
}

View file

@ -0,0 +1,36 @@
.container {
padding: 0px 7px; /*0.35rem 0.5rem */
background-color: #f6f6f6;
}
.first {
padding-top: 7px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.last {
padding-bottom: 7px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
.dashAfter {
margin-bottom: 0.8rem;
}
.referrer {
font-size: 14px;
color: $gray-dark;
font-weight: 500 !important;
display: flex;
align-items: center;
& .url {
margin-left: 5px;
font-weight: 300;
color: $gray-medium;
max-width: 70%;
overflow: hidden;
text-overflow: ellipsis;
}
}

View file

@ -0,0 +1,66 @@
.eventsBlock {
width: 270px;
margin-bottom: 5px;
}
.header {
& .hAndProgress {
display:flex;
justify-content: space-between;
align-items: center;
/* margin-bottom: 5px; */
/* height: 40px; */
& .progress {
flex: 1;
margin: 0 0 0 15px;
& :global(.bar) {
background: #ffcc99;
}
& :global(.progress) {
font-size: 9px;
}
}
}
& h5 {
margin: 0; /* get rid of semantic, please*/
font-size: 14px;
font-weight: 700;
}
}
.eventsList {
/* box-shadow: inset 0px 2px 4px rgba(0, 0, 0, 0.1); */
/* border-top: solid thin $gray-light-shade; */
&::-webkit-scrollbar {
width: 2px;
background: transparent !important;
background: rgba(0,0,0,0);
}
&::-webkit-scrollbar-thumb {
background: transparent !important;
}
&::-webkit-scrollbar-track {
background: transparent !important;
}
&:hover {
&::-webkit-scrollbar {
width: 2px;
background: rgba(0,0,0,0.1)
}
&::-webkit-scrollbar-track {
background: rgba(0,0,0,0.1)
}
&::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.1)
}
}
}
.sessionDetails {
display: flex;
font-size: 10px;
color: $gray-medium;
justify-content: space-between;
}

View file

@ -0,0 +1 @@
export { default } from './EventsBlock';

View file

@ -0,0 +1,102 @@
$green-light: #A0D6AE;
$green-middle: #859D9A;
$green-dark: #3A625E;
.bar {
display: flex;
overflow: hidden;
cursor: pointer;
/* margin: 0 -11px;
margin-bottom: -9px; */
& div {
height: 5px;
}
& div:nth-child(1) {
background-color: #C5E6E7;
}
& div:nth-child(2) {
background-color: #8BCCCF;
}
& div:nth-child(3) {
background-color :rgba(62, 170, 175, 1);
}
}
.bottomBlock {
overflow: hidden;
}
.wrapper {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 5px 12px 34px;
font-size: 13px;
/* font-weight: 500; */
& .lines {
border-bottom: 1px solid $gray-light;
border-left: 2px solid;
position: absolute;
height: 100%;
top: -21px;
left: 14px;
width: 15px;
&:before {
content: "";
border-radius: 5px;
border: 5px solid;
display: block;
width: 0;
height: 0;
position: absolute;
bottom: -5px;
left: -6px;
z-index: 1; /* in context */
}
}
}
.wrapper:nth-child(1) {
/* overflow: hidden; */
& .lines {
border-left-color: #C5E6E7;
&:before {
border-color: #C5E6E7;
}
}
}
.wrapper:nth-child(2) {
& .lines {
border-left-color: #8BCCCF;
&:before {
border-color: #8BCCCF;
}
}
}
.wrapper:nth-child(3) {
& .lines {
border-left-color: rgba(62, 170, 175, 1);
&:before {
border-color: rgba(62, 170, 175, 1);
}
}
}
.value {
font-weight: 500;
color: $gray-medium;
}
.download {
display: flex;
justify-content: space-between;
}

View file

@ -0,0 +1,145 @@
import React from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import {
sessions as sessionsRoute,
liveSession as liveSessionRoute,
withSiteId,
} from 'App/routes';
import { BackLink, Link } from 'UI';
import { toggleFavorite, setSessionPath } from 'Duck/sessions';
import cn from 'classnames';
import SessionMetaList from 'Shared/SessionItem/SessionMetaList';
import UserCard from './EventsBlock/UserCard';
import Tabs from 'Components/Session/Tabs';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import stl from './playerBlockHeader.module.css';
const SESSIONS_ROUTE = sessionsRoute();
// TODO props
function PlayerBlockHeader(props: any) {
const [hideBack, setHideBack] = React.useState(false);
const { player, store } = React.useContext(PlayerContext);
const { width, height, showEvents } = store.get();
const {
session,
fullscreen,
metaList,
closedLive = false,
siteId,
setActiveTab,
activeTab,
location,
history,
sessionPath,
} = props;
React.useEffect(() => {
const queryParams = new URLSearchParams(location.search);
setHideBack(queryParams.has('iframe') && queryParams.get('iframe') === 'true');
}, []);
const backHandler = () => {
if (
sessionPath.pathname === history.location.pathname ||
sessionPath.pathname.includes('/session/')
) {
history.push(withSiteId(SESSIONS_ROUTE, siteId));
} else {
history.push(
sessionPath ? sessionPath.pathname + sessionPath.search : withSiteId(SESSIONS_ROUTE, siteId)
);
}
};
const { sessionId, live, metadata } = session;
let _metaList = Object.keys(metadata)
.filter((i) => metaList.includes(i))
.map((key) => {
const value = metadata[key];
return { label: key, value };
});
const TABS = [props.tabs.EVENTS, props.tabs.CLICKMAP].map((tab) => ({
text: tab,
key: tab,
}));
return (
<div className={cn(stl.header, 'flex justify-between', { hidden: fullscreen })}>
<div className="flex w-full items-center">
{!hideBack && (
<div
className="flex items-center h-full cursor-pointer group"
onClick={backHandler}
>
{/* @ts-ignore TODO */}
<BackLink label="Back" className="h-full" />
<div className={stl.divider} />
</div>
)}
<UserCard className="" width={width} height={height} />
<div className={cn('ml-auto flex items-center h-full', { hidden: closedLive })}>
{live && (
<>
<div className={cn(stl.liveSwitchButton, 'pr-4')}>
<Link to={withSiteId(liveSessionRoute(sessionId), siteId)}>
This Session is Now Continuing Live
</Link>
</div>
{_metaList.length > 0 && <div className={stl.divider} />}
</>
)}
{_metaList.length > 0 && (
<div className="border-l h-full flex items-center px-2">
<SessionMetaList className="" metaList={_metaList} maxLength={2} />
</div>
)}
</div>
</div>
<div className="relative border-l" style={{ minWidth: '270px' }}>
<Tabs
tabs={TABS}
active={activeTab}
onClick={(tab) => {
if (activeTab === tab) {
setActiveTab('');
player.toggleEvents();
} else {
setActiveTab(tab);
!showEvents && player.toggleEvents();
}
}}
border={false}
/>
</div>
</div>
);
}
const PlayerHeaderCont = connect(
(state: any) => {
const session = state.getIn(['sessions', 'current']);
return {
session,
sessionPath: state.getIn(['sessions', 'sessionPath']),
local: state.getIn(['sessions', 'timezone']),
funnelRef: state.getIn(['funnels', 'navRef']),
siteId: state.getIn(['site', 'siteId']),
metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key),
};
},
{
toggleFavorite,
setSessionPath,
}
)(observer(PlayerBlockHeader));
export default withRouter(PlayerHeaderCont);

View file

@ -0,0 +1,21 @@
.header {
height: 50px;
border-bottom: solid thin $gray-light;
padding-left: 15px;
padding-right: 0;
background-color: white;
}
.divider {
width: 1px;
height: 49px;
margin: 0 10px;
background-color: $gray-light;
}
.liveSwitchButton {
cursor: pointer;
color: $green;
text-decoration: underline;
white-space: nowrap;
}

View file

@ -25,7 +25,6 @@ function WebPlayer(props: any) {
session,
toggleFullscreen,
closeBottomBlock,
live,
fullscreen,
fetchList,
customSession,
@ -37,7 +36,8 @@ function WebPlayer(props: any) {
const { notesStore } = useStore();
const [activeTab, setActiveTab] = useState('');
const [showNoteModal, setShowNote] = useState(false);
const [noteItem, setNoteItem] = useState<Note>();
const [noteItem, setNoteItem] = useState<Note | undefined>(undefined);
// @ts-ignore
const [contextValue, setContextValue] = useState<IPlayerContext>(defaultContextValue);
useEffect(() => {
@ -55,11 +55,10 @@ function WebPlayer(props: any) {
if (!isClickmap) {
notesStore.fetchSessionNotes(session.sessionId).then((r) => {
const noteId = props.query.get('note');
const note = notesStore.getNoteById(parseInt(noteId, 10), r)
const note = props.query.get('note');
if (note) {
WebPlayerInst.pause();
setNoteItem(note);
setNoteItem(notesStore.getNoteById(parseInt(note, 10), r));
setShowNote(true);
}
});
@ -124,7 +123,6 @@ function WebPlayer(props: any) {
<PlayerContent
activeTab={activeTab}
fullscreen={fullscreen}
live={live}
setActiveTab={setActiveTab}
session={session}
isClickmap={isClickmap}

View file

@ -7,9 +7,18 @@ import {
} from 'Player'
export interface IPlayerContext {
player: IWebPlayer | IWebLivePlayer
store: IWebPlayerStore | IWebLivePlayerStore,
player: IWebPlayer
store: IWebPlayerStore,
}
export interface ILivePlayerContext {
player: IWebLivePlayer
store: IWebLivePlayerStore
}
type ContextType =
| IPlayerContext
| ILivePlayerContext
export const defaultContextValue = { player: undefined, store: undefined}
// @ts-ignore
export const PlayerContext = createContext<IPlayerContext>(defaultContextValue);
export const PlayerContext = createContext<ContextType>(defaultContextValue);

View file

@ -8,7 +8,7 @@ import { TeamBadge } from 'Shared/SessionListContainer/components/Notes';
interface Props {
userEmail: string;
note: Note;
note?: Note;
notFound?: boolean;
onClose: () => void;
}
@ -17,7 +17,7 @@ function ReadNote(props: Props) {
const { settingsStore } = useStore();
const { timezone } = settingsStore.sessionSettings;
if (props.notFound) {
if (props.notFound || props.note === undefined) {
return (
<div style={{ position: 'absolute', top: '45%', left: 'calc(50% - 200px)' }}>
<div

View file

@ -1,298 +0,0 @@
import { storiesOf } from '@storybook/react';
import { List } from 'immutable';
import EventGroup from './EventsBlock/Event';
const groups = [
{
"page": {
"key": "Location_257",
"time": 2751,
"type": "LOCATION",
"url": "/login",
"pageLoad": false,
"fcpTime": 6787,
"loadTime": 7872,
"domTime": 5821,
"referrer": "Search Engine"
},
"events": [
{
"sessionId": 2406625057772570,
"messageId": 76446,
"timestamp": 1586722257371,
"label": "Device Memory: 8.19GB",
"type": "CLICKRAGE",
"count": 3
},
{
"key": "Click_256",
"time": 13398,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_262",
"path": "",
"label": null
}
},
{
"key": "Input_256",
"time": 13438,
"type": "INPUT",
"target": {
"key": "record_263",
"path": "",
"label": null
},
"value": null
}
]
},
{
"page": {
"key": "Location_258",
"time": 15841,
"type": "LOCATION",
"url": "/1/sessions",
"pageLoad": false,
"fcpTime": null,
"loadTime": null,
"domTime": null,
"referrer": ""
},
"events": [
{
"key": "Click_257",
"time": 24408,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_264",
"path": "",
"label": null
}
}
]
},
{
"page": {
"key": "Location_259",
"time": 25019,
"type": "LOCATION",
"url": "/1/session/2303531983744788",
"pageLoad": false,
"fcpTime": null,
"loadTime": null,
"domTime": null,
"referrer": ""
},
"events": [
{
"key": "Click_258",
"time": 31134,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_265",
"path": "",
"label": null
}
},
{
"key": "Click_259",
"time": 32022,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_266",
"path": "",
"label": null
}
},
{
"key": "Click_260",
"time": 35951,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_267",
"path": "",
"label": null
}
},
{
"key": "Click_261",
"time": 164029,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_268",
"path": "",
"label": null
}
},
{
"key": "Click_262",
"time": 169739,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_269",
"path": "",
"label": null
}
},
{
"key": "Click_263",
"time": 170524,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_270",
"path": "",
"label": null
}
},
{
"key": "Click_264",
"time": 172580,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_271",
"path": "",
"label": null
}
},
{
"key": "Click_265",
"time": 173102,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_272",
"path": "",
"label": null
}
},
{
"key": "Click_266",
"time": 173698,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_273",
"path": "",
"label": null
}
},
{
"key": "Click_267",
"time": 173867,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_274",
"path": "",
"label": null
}
},
{
"key": "Click_268",
"time": 174599,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_275",
"path": "",
"label": null
}
},
{
"key": "Click_269",
"time": 175148,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_276",
"path": "",
"label": null
}
},
{
"key": "Click_270",
"time": 175779,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_277",
"path": "",
"label": null
}
},
{
"key": "Click_271",
"time": 176658,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_278",
"path": "",
"label": null
}
},
{
"key": "Click_272",
"time": 177267,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_279",
"path": "",
"label": null
}
},
{
"key": "Click_273",
"time": 187025,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_280",
"path": "",
"label": null
}
},
{
"key": "Click_274",
"time": 189787,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_281",
"path": "",
"label": null
}
},
{
"key": "Click_275",
"time": 191326,
"type": "CLICK",
"targetContent": "",
"target": {
"key": "record_282",
"path": "",
"label": null
}
}
]
}
]
// storiesOf('Player', module)
// .add('Event Group', () => (
// <EventGroup
// group={groups[0]}
// selectedEvents={[]}
// />
// ))

View file

@ -401,7 +401,7 @@ export default class MessageManager {
case MType.Fetch:
case MType.NetworkRequest:
// @ts-ignore burn immutable
this.lists.lists.fetch.insert(Resource({
this.lists.lists.fetch.insert(new Resource({
method: msg.method,
url: msg.url,
request: msg.request,