refactor(ui/player): refactor more components

This commit is contained in:
sylenien 2022-11-25 11:02:34 +01:00
parent e4af3bd6a3
commit 5b97a0c3cf
26 changed files with 1039 additions and 1034 deletions

View file

@ -2,7 +2,7 @@ import React from 'react';
import { INDEXES } from 'App/constants/zindex';
import { connect } from 'react-redux';
import { Button, Loader, Icon } from 'UI';
import { initiateCallEnd, releaseRemoteControl } from 'Player';
import { PlayerContext } from 'App/components/Session/playerContext';
interface Props {
userDisplayName: string;
@ -14,20 +14,38 @@ export enum WindowType {
Control,
}
enum Actions {
CallEnd,
ControlEnd
}
const WIN_VARIANTS = {
[WindowType.Call]: {
text: 'to accept the call',
icon: 'call' as const,
action: initiateCallEnd,
action: Actions.CallEnd,
},
[WindowType.Control]: {
text: 'to accept remote control request',
icon: 'remote-control' as const,
action: releaseRemoteControl,
action: Actions.ControlEnd,
},
};
function RequestingWindow({ userDisplayName, type }: Props) {
const { player } = React.useContext(PlayerContext)
const {
assistManager: {
initiateCallEnd,
releaseRemoteControl,
}
} = player
const actions = {
[Actions.CallEnd]: initiateCallEnd,
[Actions.ControlEnd]: releaseRemoteControl
}
return (
<div
className="w-full h-full absolute top-0 left-0 flex items-center justify-center"
@ -40,7 +58,7 @@ function RequestingWindow({ userDisplayName, type }: Props) {
</div>
<span>{WIN_VARIANTS[type].text}</span>
<Loader size={30} style={{ minHeight: 60 }} />
<Button variant="text-primary" onClick={WIN_VARIANTS[type].action}>
<Button variant="text-primary" onClick={actions[WIN_VARIANTS[type].action]}>
Cancel
</Button>
</div>
@ -48,6 +66,6 @@ function RequestingWindow({ userDisplayName, type }: Props) {
);
}
export default connect((state) => ({
export default connect((state: any) => ({
userDisplayName: state.getIn(['sessions', 'current', 'userDisplayName']),
}))(RequestingWindow);

View file

@ -3,15 +3,8 @@ import { Button, Tooltip } from 'UI';
import { connect } from 'react-redux';
import cn from 'classnames';
import { toggleChatWindow } from 'Duck/sessions';
import { connectPlayer } from 'Player';
import ChatWindow from '../../ChatWindow';
import {
callPeer,
setCallArgs,
requestReleaseRemoteControl,
toggleAnnotation,
toggleUserName,
} from 'Player';
// state enums
import {
CallingState,
ConnectionStatus,
@ -19,6 +12,8 @@ import {
RequestLocalStream,
} from 'Player';
import type { LocalStream } from 'Player';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { toast } from 'react-toastify';
import { confirm } from 'UI';
import stl from './AassistActions.module.css';
@ -48,17 +43,31 @@ interface Props {
function AssistActions({
userId,
calling,
annotating,
peerConnectionStatus,
remoteControlStatus,
hasPermission,
isEnterprise,
isCallActive,
agentIds,
livePlay,
userDisplayName,
}: Props) {
const { player, store } = React.useContext(PlayerContext)
const {
assistManager: {
call: callPeer,
setCallArgs,
requestReleaseRemoteControl,
toggleAnnotation,
},
toggleUserName,
} = player
const {
calling,
annotating,
peerConnectionStatus,
remoteControl: remoteControlStatus,
livePlay,
} = store.get()
const [isPrestart, setPrestart] = useState(false);
const [incomeStream, setIncomeStream] = useState<MediaStream[] | null>([]);
const [localStream, setLocalStream] = useState<LocalStream | null>(null);
@ -236,7 +245,7 @@ function AssistActions({
}
const con = connect(
(state) => {
(state: any) => {
const permissions = state.getIn(['user', 'account', 'permissions']) || [];
return {
hasPermission: permissions.includes('ASSIST_CALL'),
@ -248,11 +257,5 @@ const con = connect(
);
export default con(
connectPlayer((state) => ({
calling: state.calling,
annotating: state.annotating,
remoteControlStatus: state.remoteControl,
peerConnectionStatus: state.peerConnectionStatus,
livePlay: state.livePlay,
}))(AssistActions)
observer(AssistActions)
);

View file

@ -1,25 +1,20 @@
import React from 'react';
import { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { Loader } from 'UI';
import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player';
import { withRequest } from 'HOCs'
import {
PlayerProvider,
connectPlayer,
init as initPlayer,
clean as cleanPlayer,
} from 'Player';
import withPermissions from 'HOCs/withPermissions';
import { PlayerContext, defaultContextValue } from './playerContext';
import { createLiveWebPlayer } from 'Player';
import { makeAutoObservable } from 'mobx';
import PlayerBlockHeader from '../Session_/PlayerBlockHeader';
import PlayerBlock from '../Session_/PlayerBlock';
import styles from '../Session_/session.module.css';
const InitLoader = connectPlayer(state => ({
loading: !state.initialized
}))(Loader);
function LivePlayer ({
session,
toggleFullscreen,
@ -32,6 +27,8 @@ function LivePlayer ({
userEmail,
userName
}) {
const [contextValue, setContextValue] = useState<IPlayerContext>(defaultContextValue);
useEffect(() => {
if (!loadingCredentials) {
@ -42,9 +39,16 @@ function LivePlayer ({
name: userName,
},
}
initPlayer(sessionWithAgentData, assistCredendials, true);
// initPlayer(sessionWithAgentData, assistCredendials, true);
const [LivePlayer, LivePlayerStore] = createLiveWebPlayer(
sessionWithAgentData,
assistCredendials,
(state) => makeAutoObservable(state)
)
setContextValue({ player: LivePlayer, store: LivePlayerStore });
}
return () => cleanPlayer()
return () => LivePlayer.clean()
}, [ session.sessionId, loadingCredentials, assistCredendials ]);
// LAYOUT (TODO: local layout state - useContext or something..)
@ -64,15 +68,17 @@ function LivePlayer ({
}
const [activeTab, setActiveTab] = useState('');
if (!contextValue.player) return null;
return (
<PlayerProvider>
<InitLoader className="flex-1 p-3">
<PlayerBlockHeader activeTab={activeTab} setActiveTab={setActiveTab} tabs={TABS} fullscreen={fullscreen}/>
<div className={ styles.session } data-fullscreen={fullscreen}>
<PlayerBlock />
</div>
</InitLoader>
</PlayerProvider>
<PlayerContext.Provider value={contextValue}>
<PlayerProvider>
<PlayerBlockHeader activeTab={activeTab} setActiveTab={setActiveTab} tabs={TABS} fullscreen={fullscreen}/>
<div className={ styles.session } data-fullscreen={fullscreen}>
<PlayerBlock />
</div>
</PlayerProvider>
</PlayerContext.Provider>
);
};

View file

@ -5,7 +5,6 @@ import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player';
import { fetchList } from 'Duck/integrations';
import { PlayerProvider, createWebPlayer } from 'Player';
import { makeAutoObservable } from 'mobx';
import { observer } from 'mobx-react-lite';
import withLocationHandlers from 'HOCs/withLocationHandlers';
import { useStore } from 'App/mstore';
import PlayerBlockHeader from '../Session_/PlayerBlockHeader';
@ -85,6 +84,7 @@ function WebPlayer(props: any) {
<PlayerProvider>
<>
<PlayerBlockHeader
// @ts-ignore TODO?
activeTab={activeTab}
setActiveTab={setActiveTab}
tabs={TABS}

View file

@ -1,6 +1,6 @@
import React from 'react';
import { Icon } from 'UI';
import { tagProps, iTag, Note } from 'App/services/NotesService';
import { tagProps, Note } from 'App/services/NotesService';
import { formatTimeOrDate } from 'App/date';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';

View file

@ -55,16 +55,6 @@ export default class Exceptions extends React.PureComponent {
const filtered = exceptions.filter((e) => filterRE.test(e.name) || filterRE.test(e.message));
// let lastIndex = -1;
// filtered.forEach((item, index) => {
// if (
// this.props.exceptionsNow.length > 0 &&
// item.time <= this.props.exceptionsNow[this.props.exceptionsNow.length - 1].time
// ) {
// lastIndex = index;
// }
// });
return (
<>
<SlideModal
@ -140,8 +130,6 @@ export default class Exceptions extends React.PureComponent {
onJump={() => jump(e.time)}
error={e}
key={e.key}
// selected={lastIndex === index}
// inactive={index > lastIndex}
onErrorClick={(jsEvent) => {
jsEvent.stopPropagation();
jsEvent.preventDefault();

View file

@ -38,7 +38,7 @@ function OverviewPanel({ issuesList }: { issuesList: any[] }) {
const resourceList = resourceListUnmap
.filter((r: any) => r.isRed() || r.isYellow())
.concat(fetchList.filter((i: any) => parseInt(i.status) >= 400))
.concat(graphqlList.filter((i: any) => parseInt(i.status) >= 400)),
.concat(graphqlList.filter((i: any) => parseInt(i.status) >= 400))
const resources: any = React.useMemo(() => {
return {

View file

@ -1,9 +1,9 @@
import React, { useState } from 'react';
import React from 'react';
import stl from './SelectorCard.module.css';
import cn from 'classnames';
import type { MarkedTarget } from 'Player';
import { activeTarget } from 'Player';
import { Tooltip } from 'react-tippy';
import { PlayerContext } from 'App/components/Session/playerContext';
interface Props {
index?: number;
@ -12,7 +12,11 @@ interface Props {
}
export default function SelectorCard({ index = 1, target, showContent }: Props) {
const { player } = React.useContext(PlayerContext)
const activeTarget = player.setActiveTarget
return (
// @ts-ignore TODO for Alex
<div className={cn(stl.wrapper, { [stl.active]: showContent })} onClick={() => activeTarget(index)}>
<div className={stl.top}>
{/* @ts-ignore */}

View file

@ -12,7 +12,7 @@ const ControlButton = ({
hasErrors = false,
active = false,
size = 20,
noLabel,
noLabel = false,
labelClassName,
containerClassName,
noIcon,

View file

@ -1,434 +0,0 @@
import React from 'react';
import cn from 'classnames';
import { connect } from 'react-redux';
import {
connectPlayer,
STORAGE_TYPES,
selectStorageType,
selectStorageListNow,
jumpToLive,
toggleInspectorMode,
} from 'Player';
import LiveTag from 'Shared/LiveTag';
import { Icon, Tooltip } from 'UI';
import {
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
changeSkipInterval,
OVERVIEW,
CONSOLE,
NETWORK,
STACKEVENTS,
STORAGE,
PROFILER,
PERFORMANCE,
GRAPHQL,
INSPECTOR,
} from 'Duck/components/player';
import { AssistDuration } from './Time';
import Timeline from './Timeline';
import ControlButton from './ControlButton';
import PlayerControls from './components/PlayerControls';
import styles from './controls.module.css';
import XRayButton from 'Shared/XRayButton';
const SKIP_INTERVALS = {
2: 2e3,
5: 5e3,
10: 1e4,
15: 15e3,
20: 2e4,
30: 3e4,
60: 6e4,
};
function getStorageName(type) {
switch (type) {
case STORAGE_TYPES.REDUX:
return 'REDUX';
case STORAGE_TYPES.MOBX:
return 'MOBX';
case STORAGE_TYPES.VUEX:
return 'VUEX';
case STORAGE_TYPES.NGRX:
return 'NGRX';
case STORAGE_TYPES.ZUSTAND:
return 'ZUSTAND';
case STORAGE_TYPES.NONE:
return 'STATE';
}
}
@connectPlayer((state) => ({
time: state.time,
endTime: state.endTime,
live: state.live,
livePlay: state.livePlay,
playing: state.playing,
completed: state.completed,
skip: state.skip,
skipToIssue: state.skipToIssue,
speed: state.speed,
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode || state.markedTargets,
inspectorMode: state.inspectorMode,
fullscreenDisabled: state.messagesLoading,
// logCount: state.logList.length,
logRedCount: state.logRedCount,
showExceptions: state.exceptionsList.length > 0,
resourceRedCount: state.resourceRedCount,
fetchRedCount: state.fetchRedCount,
showStack: state.stackList.length > 0,
stackCount: state.stackList.length,
stackRedCount: state.stackRedCount,
profilesCount: state.profilesList.length,
storageCount: selectStorageListNow(state).length,
storageType: selectStorageType(state),
showStorage: selectStorageType(state) !== STORAGE_TYPES.NONE,
showProfiler: state.profilesList.length > 0,
showGraphql: state.graphqlList.length > 0,
showFetch: state.fetchCount > 0,
fetchCount: state.fetchCount,
graphqlCount: state.graphqlList.length,
liveTimeTravel: state.liveTimeTravel,
}))
@connect(
(state, props) => {
const permissions = state.getIn(['user', 'account', 'permissions']) || [];
const isEnterprise = state.getIn(['user', 'account', 'edition']) === 'ee';
return {
disabled: props.disabled || (isEnterprise && !permissions.includes('DEV_TOOLS')),
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
bottomBlock: state.getIn(['components', 'player', 'bottomBlock']),
showStorage:
props.showStorage || !state.getIn(['components', 'player', 'hiddenHints', 'storage']),
showStack: props.showStack || !state.getIn(['components', 'player', 'hiddenHints', 'stack']),
closedLive:
!!state.getIn(['sessions', 'errors']) || !state.getIn(['sessions', 'current', 'live']),
skipInterval: state.getIn(['components', 'player', 'skipInterval']),
};
},
{
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
changeSkipInterval,
}
)
export default class Controls extends React.Component {
componentDidMount() {
document.addEventListener('keydown', this.onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onKeyDown);
//this.props.toggleInspectorMode(false);
}
shouldComponentUpdate(nextProps) {
if (
nextProps.fullscreen !== this.props.fullscreen ||
nextProps.bottomBlock !== this.props.bottomBlock ||
nextProps.live !== this.props.live ||
nextProps.livePlay !== this.props.livePlay ||
nextProps.playing !== this.props.playing ||
nextProps.completed !== this.props.completed ||
nextProps.skip !== this.props.skip ||
nextProps.skipToIssue !== this.props.skipToIssue ||
nextProps.speed !== this.props.speed ||
nextProps.disabled !== this.props.disabled ||
nextProps.fullscreenDisabled !== this.props.fullscreenDisabled ||
// nextProps.inspectorMode !== this.props.inspectorMode ||
// nextProps.logCount !== this.props.logCount ||
nextProps.logRedCount !== this.props.logRedCount ||
nextProps.showExceptions !== this.props.showExceptions ||
nextProps.resourceRedCount !== this.props.resourceRedCount ||
nextProps.fetchRedCount !== this.props.fetchRedCount ||
nextProps.showStack !== this.props.showStack ||
nextProps.stackCount !== this.props.stackCount ||
nextProps.stackRedCount !== this.props.stackRedCount ||
nextProps.profilesCount !== this.props.profilesCount ||
nextProps.storageCount !== this.props.storageCount ||
nextProps.storageType !== this.props.storageType ||
nextProps.showStorage !== this.props.showStorage ||
nextProps.showProfiler !== this.props.showProfiler ||
nextProps.showGraphql !== this.props.showGraphql ||
nextProps.showFetch !== this.props.showFetch ||
nextProps.fetchCount !== this.props.fetchCount ||
nextProps.graphqlCount !== this.props.graphqlCount ||
nextProps.liveTimeTravel !== this.props.liveTimeTravel ||
nextProps.skipInterval !== this.props.skipInterval
)
return true;
return false;
}
onKeyDown = (e) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
if (this.props.inspectorMode) {
if (e.key === 'Esc' || e.key === 'Escape') {
toggleInspectorMode(false);
}
}
// if (e.key === ' ') {
// document.activeElement.blur();
// this.props.togglePlay();
// }
if (e.key === 'Esc' || e.key === 'Escape') {
this.props.fullscreenOff();
}
if (e.key === 'ArrowRight') {
this.forthTenSeconds();
}
if (e.key === 'ArrowLeft') {
this.backTenSeconds();
}
if (e.key === 'ArrowDown') {
this.props.speedDown();
}
if (e.key === 'ArrowUp') {
this.props.speedUp();
}
};
forthTenSeconds = () => {
const { time, endTime, jump, skipInterval } = this.props;
jump(Math.min(endTime, time + SKIP_INTERVALS[skipInterval]));
};
backTenSeconds = () => {
//shouldComponentUpdate
const { time, jump, skipInterval } = this.props;
jump(Math.max(0, time - SKIP_INTERVALS[skipInterval]));
};
goLive = () => this.props.jump(this.props.endTime);
renderPlayBtn = () => {
const { completed, playing } = this.props;
let label;
let icon;
if (completed) {
icon = 'arrow-clockwise';
label = 'Replay this session';
} else if (playing) {
icon = 'pause-fill';
label = 'Pause';
} else {
icon = 'play-fill-new';
label = 'Pause';
label = 'Play';
}
return (
<Tooltip
title={label}
className="mr-4"
>
<div
onClick={this.props.togglePlay}
className="hover-main color-main cursor-pointer rounded hover:bg-gray-light-shade"
>
<Icon name={icon} size="36" color="inherit" />
</div>
</Tooltip>
);
};
controlIcon = (icon, size, action, isBackwards, additionalClasses) => (
<div
onClick={action}
className={cn('py-1 px-2 hover-main cursor-pointer bg-gray-lightest', additionalClasses)}
style={{ transform: isBackwards ? 'rotate(180deg)' : '' }}
>
<Icon name={icon} size={size} color="inherit" />
</div>
);
render() {
const {
bottomBlock,
toggleBottomBlock,
live,
livePlay,
skip,
speed,
disabled,
logRedCount,
showExceptions,
resourceRedCount,
showStack,
stackRedCount,
showStorage,
storageType,
showProfiler,
showGraphql,
fullscreen,
inspectorMode,
closedLive,
toggleSpeed,
toggleSkip,
liveTimeTravel,
changeSkipInterval,
skipInterval,
} = this.props;
const toggleBottomTools = (blockName) => {
if (blockName === INSPECTOR) {
toggleInspectorMode();
bottomBlock && toggleBottomBlock();
} else {
toggleInspectorMode(false);
toggleBottomBlock(blockName);
}
};
return (
<div className={styles.controls}>
<Timeline
live={live}
jump={this.props.jump}
liveTimeTravel={liveTimeTravel}
pause={this.props.pause}
togglePlay={this.props.togglePlay}
/>
{!fullscreen && (
<div className={cn(styles.buttons, { '!px-5 !pt-0': live })} data-is-live={live}>
<div className="flex items-center">
{!live && (
<>
<PlayerControls
live={live}
skip={skip}
speed={speed}
disabled={disabled}
backTenSeconds={this.backTenSeconds}
forthTenSeconds={this.forthTenSeconds}
toggleSpeed={toggleSpeed}
toggleSkip={toggleSkip}
playButton={this.renderPlayBtn()}
controlIcon={this.controlIcon}
ref={this.speedRef}
skipIntervals={SKIP_INTERVALS}
setSkipInterval={changeSkipInterval}
currentInterval={skipInterval}
/>
<div className={cn('mx-2')} />
<XRayButton
isActive={bottomBlock === OVERVIEW && !inspectorMode}
onClick={() => toggleBottomTools(OVERVIEW)}
/>
</>
)}
{live && !closedLive && (
<div className={styles.buttonsLeft}>
<LiveTag isLive={livePlay} onClick={() => (livePlay ? null : jumpToLive())} />
<div className="font-semibold px-2">
<AssistDuration isLivePlay={livePlay} />
</div>
</div>
)}
</div>
<div className="flex items-center h-full">
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(CONSOLE)}
active={bottomBlock === CONSOLE && !inspectorMode}
label="CONSOLE"
noIcon
labelClassName="!text-base font-semibold"
hasErrors={logRedCount > 0 || showExceptions}
containerClassName="mx-2"
/>
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(NETWORK)}
active={bottomBlock === NETWORK && !inspectorMode}
label="NETWORK"
hasErrors={resourceRedCount > 0}
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(PERFORMANCE)}
active={bottomBlock === PERFORMANCE && !inspectorMode}
label="PERFORMANCE"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && showGraphql && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(GRAPHQL)}
active={bottomBlock === GRAPHQL && !inspectorMode}
label="GRAPHQL"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && showStorage && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(STORAGE)}
active={bottomBlock === STORAGE && !inspectorMode}
label={getStorageName(storageType)}
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(STACKEVENTS)}
active={bottomBlock === STACKEVENTS && !inspectorMode}
label="EVENTS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
hasErrors={stackRedCount > 0}
/>
)}
{!live && showProfiler && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(PROFILER)}
active={bottomBlock === PROFILER && !inspectorMode}
label="PROFILER"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && (
<Tooltip title="Fullscreen" delay={0} position="top-end" className="mx-4">
{this.controlIcon(
'arrows-angle-extend',
16,
this.props.fullscreenOn,
false,
'rounded hover:bg-gray-light-shade color-gray-medium'
)}
</Tooltip>
)}
</div>
</div>
)}
</div>
);
}
}

View file

@ -0,0 +1,420 @@
import React from 'react';
import cn from 'classnames';
import { connect } from 'react-redux';
import { STORAGE_TYPES, selectStorageType, selectStorageListNow } from 'Player';
import LiveTag from 'Shared/LiveTag';
import { Icon, Tooltip } from 'UI';
import {
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
changeSkipInterval,
OVERVIEW,
CONSOLE,
NETWORK,
STACKEVENTS,
STORAGE,
PROFILER,
PERFORMANCE,
GRAPHQL,
INSPECTOR,
} from 'Duck/components/player';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { AssistDuration } from './Time';
import Timeline from './Timeline';
import ControlButton from './ControlButton';
import PlayerControls from './components/PlayerControls';
import styles from './controls.module.css';
import XRayButton from 'Shared/XRayButton';
const SKIP_INTERVALS = {
2: 2e3,
5: 5e3,
10: 1e4,
15: 15e3,
20: 2e4,
30: 3e4,
60: 6e4,
};
function getStorageName(type: any) {
switch (type) {
case STORAGE_TYPES.REDUX:
return 'REDUX';
case STORAGE_TYPES.MOBX:
return 'MOBX';
case STORAGE_TYPES.VUEX:
return 'VUEX';
case STORAGE_TYPES.NGRX:
return 'NGRX';
case STORAGE_TYPES.ZUSTAND:
return 'ZUSTAND';
case STORAGE_TYPES.NONE:
return 'STATE';
}
}
function Controls(props: any) {
const { player, store } = React.useContext(PlayerContext);
const { jumpToLive, toggleInspectorMode } = player;
const {
live,
livePlay,
playing,
completed,
skip,
// skipToIssue, UPDATE
speed,
cssLoading,
messagesLoading,
inspectorMode,
markedTargets,
// messagesLoading: fullscreenDisabled, UPDATE
stackList,
exceptionsList,
profilesList,
graphqlList,
fetchList,
liveTimeTravel,
logMarkedCountNow: logRedCount,
resourceMarkedCountNow: resourceRedCount,
stackMarkedCountNow: stackRedCount,
} = store.get();
// const storageCount = selectStorageListNow(store.get()).length UPDATE
const {
bottomBlock,
toggleBottomBlock,
fullscreen,
closedLive,
changeSkipInterval,
skipInterval,
disabledRedux,
showStorageRedux,
showStackRedux,
} = props;
const storageType = selectStorageType(store.get());
const disabled = disabledRedux || cssLoading || messagesLoading || inspectorMode || markedTargets;
const stackCount = stackList.length;
const profilesCount = profilesList.length;
const graphqlCount = graphqlList.length;
const showGraphql = graphqlCount > 0;
const fetchCount = fetchList.length;
const showProfiler = profilesCount > 0;
const showExceptions = exceptionsList.length > 0;
const showStorage = storageType !== STORAGE_TYPES.NONE || showStorageRedux;
// const showStack = stackCount > 0 || showStackRedux UPDATE
// const showFetch = fetchCount > 0 UPDATE
const onKeyDown = (e: any) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
if (inspectorMode) {
if (e.key === 'Esc' || e.key === 'Escape') {
player.toggleInspectorMode(false);
}
}
if (e.key === 'Esc' || e.key === 'Escape') {
props.fullscreenOff();
}
if (e.key === 'ArrowRight') {
forthTenSeconds();
}
if (e.key === 'ArrowLeft') {
backTenSeconds();
}
if (e.key === 'ArrowDown') {
props.speedDown();
}
if (e.key === 'ArrowUp') {
props.speedUp();
}
};
React.useEffect(() => {
document.addEventListener('keydown', onKeyDown);
return () => {
document.removeEventListener('keydown', onKeyDown);
};
}, []);
const forthTenSeconds = () => {
// @ts-ignore
player.jumpInterval(SKIP_INTERVALS[skipInterval])
};
const backTenSeconds = () => {
// @ts-ignore
player.jumpInterval(-SKIP_INTERVALS[skipInterval])
};
const renderPlayBtn = () => {
let label;
let icon;
if (completed) {
icon = 'arrow-clockwise' as const;
label = 'Replay this session';
} else if (playing) {
icon = 'pause-fill' as const;
label = 'Pause';
} else {
icon = 'play-fill-new' as const;
label = 'Pause';
label = 'Play';
}
return (
<Tooltip title={label} className="mr-4">
<div
onClick={() => player.togglePlay()}
className="hover-main color-main cursor-pointer rounded hover:bg-gray-light-shade"
>
<Icon name={icon} size="36" color="inherit" />
</div>
</Tooltip>
);
};
const controlIcon = (
icon: string,
size: number,
action: (args: any) => any,
isBackwards: boolean,
additionalClasses: string
) => (
<div
onClick={action}
className={cn('py-1 px-2 hover-main cursor-pointer bg-gray-lightest', additionalClasses)}
style={{ transform: isBackwards ? 'rotate(180deg)' : '' }}
>
<Icon
// @ts-ignore
name={icon}
size={size}
color="inherit"
/>
</div>
);
const toggleBottomTools = (blockName: number) => {
if (blockName === INSPECTOR) {
toggleInspectorMode(false);
bottomBlock && toggleBottomBlock();
} else {
toggleInspectorMode(false);
toggleBottomBlock(blockName);
}
};
return (
<div className={styles.controls}>
<Timeline
live={live}
jump={(t: number) => player.jump(t)}
liveTimeTravel={liveTimeTravel}
pause={() => player.pause()}
togglePlay={() => player.togglePlay()}
/>
{!fullscreen && (
<div className={cn(styles.buttons, live ? '!px-5 !pt-0' : '!px-2')} data-is-live={live}>
<div className="flex items-center">
{!live && (
<>
<PlayerControls
live={live}
skip={skip}
speed={speed}
disabled={disabled}
backTenSeconds={backTenSeconds}
forthTenSeconds={forthTenSeconds}
toggleSpeed={() => player.toggleSpeed()}
toggleSkip={() => player.toggleSkip()}
playButton={renderPlayBtn()}
controlIcon={controlIcon}
skipIntervals={SKIP_INTERVALS}
setSkipInterval={changeSkipInterval}
currentInterval={skipInterval}
/>
<div className={cn('mx-2')} />
<XRayButton
isActive={bottomBlock === OVERVIEW && !inspectorMode}
onClick={() => toggleBottomTools(OVERVIEW)}
/>
</>
)}
{live && !closedLive && (
<div className={styles.buttonsLeft}>
<LiveTag isLive={livePlay} onClick={() => (livePlay ? null : jumpToLive())} />
<div className="font-semibold px-2">
<AssistDuration />
</div>
</div>
)}
</div>
<div className="flex items-center h-full">
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(CONSOLE)}
active={bottomBlock === CONSOLE && !inspectorMode}
label="CONSOLE"
noIcon
labelClassName="!text-base font-semibold"
hasErrors={logRedCount > 0 || showExceptions}
containerClassName="mx-2"
/>
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(NETWORK)}
active={bottomBlock === NETWORK && !inspectorMode}
label="NETWORK"
hasErrors={resourceRedCount > 0}
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(PERFORMANCE)}
active={bottomBlock === PERFORMANCE && !inspectorMode}
label="PERFORMANCE"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && showGraphql && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(GRAPHQL)}
active={bottomBlock === GRAPHQL && !inspectorMode}
label="GRAPHQL"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && showStorage && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(STORAGE)}
active={bottomBlock === STORAGE && !inspectorMode}
label={getStorageName(storageType)}
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(STACKEVENTS)}
active={bottomBlock === STACKEVENTS && !inspectorMode}
label="EVENTS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
hasErrors={stackRedCount > 0}
/>
)}
{!live && showProfiler && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(PROFILER)}
active={bottomBlock === PROFILER && !inspectorMode}
label="PROFILER"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && (
<Tooltip title="Fullscreen" delay={0} placement="top-start" className="mx-4">
{controlIcon(
'arrows-angle-extend',
16,
props.fullscreenOn,
false,
'rounded hover:bg-gray-light-shade color-gray-medium'
)}
</Tooltip>
)}
</div>
</div>
)}
</div>
);
}
const ControlPlayer = observer(Controls);
export default connect(
(state: any) => {
const permissions = state.getIn(['user', 'account', 'permissions']) || [];
const isEnterprise = state.getIn(['user', 'account', 'edition']) === 'ee';
return {
disabledRedux: isEnterprise && !permissions.includes('DEV_TOOLS'),
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
bottomBlock: state.getIn(['components', 'player', 'bottomBlock']),
showStorageRedux: !state.getIn(['components', 'player', 'hiddenHints', 'storage']),
showStackRedux: !state.getIn(['components', 'player', 'hiddenHints', 'stack']),
closedLive:
!!state.getIn(['sessions', 'errors']) || !state.getIn(['sessions', 'current', 'live']),
skipInterval: state.getIn(['components', 'player', 'skipInterval']),
};
},
{
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
changeSkipInterval,
}
)(ControlPlayer);
// shouldComponentUpdate(nextProps) {
// if (
// nextProps.fullscreen !== props.fullscreen ||
// nextProps.bottomBlock !== props.bottomBlock ||
// nextProps.live !== props.live ||
// nextProps.livePlay !== props.livePlay ||
// nextProps.playing !== props.playing ||
// nextProps.completed !== props.completed ||
// nextProps.skip !== props.skip ||
// nextProps.skipToIssue !== props.skipToIssue ||
// nextProps.speed !== props.speed ||
// nextProps.disabled !== props.disabled ||
// nextProps.fullscreenDisabled !== props.fullscreenDisabled ||
// // nextProps.inspectorMode !== props.inspectorMode ||
// // nextProps.logCount !== props.logCount ||
// nextProps.logRedCount !== props.logRedCount ||
// nextProps.showExceptions !== props.showExceptions ||
// nextProps.resourceRedCount !== props.resourceRedCount ||
// nextProps.fetchRedCount !== props.fetchRedCount ||
// nextProps.showStack !== props.showStack ||
// nextProps.stackCount !== props.stackCount ||
// nextProps.stackRedCount !== props.stackRedCount ||
// nextProps.profilesCount !== props.profilesCount ||
// nextProps.storageCount !== props.storageCount ||
// nextProps.storageType !== props.storageType ||
// nextProps.showStorage !== props.showStorage ||
// nextProps.showProfiler !== props.showProfiler ||
// nextProps.showGraphql !== props.showGraphql ||
// nextProps.showFetch !== props.showFetch ||
// nextProps.fetchCount !== props.fetchCount ||
// nextProps.graphqlCount !== props.graphqlCount ||
// nextProps.liveTimeTravel !== props.liveTimeTravel ||
// nextProps.skipInterval !== props.skipInterval
// )
// return true;
// return false;
// }

View file

@ -12,21 +12,17 @@ const Time = ({ time, isCustom, format = 'm:ss', }) => (
Time.displayName = "Time";
const ReduxTime = observer(({ format, name }) => {
const ReduxTime = observer(({ format, name, isCustom }) => {
const { store } = React.useContext(PlayerContext)
const time = store.get()[name]
return <Time format={format} time={time} />
return <Time format={format} time={time} isCustom={isCustom} />
})
const AssistDurationCont = connectPlayer(
state => {
const assistStart = state.assistStart;
return {
assistStart,
}
}
)(({ assistStart }) => {
const AssistDurationCont = () => {
const { store } = React.useContext(PlayerContext)
const { assistStart } = store.get()
const [assistDuration, setAssistDuration] = React.useState('00:00');
React.useEffect(() => {
const interval = setInterval(() => {
@ -40,9 +36,9 @@ const AssistDurationCont = connectPlayer(
Elapsed {assistDuration}
</>
)
})
}
const AssistDuration = React.memo(AssistDurationCont);
const AssistDuration = observer(AssistDurationCont)
ReduxTime.displayName = "ReduxTime";

View file

@ -94,7 +94,7 @@ function PlayerControls(props: Props) {
<div className="rounded ml-4 bg-active-blue border border-active-blue-border flex items-stretch">
{/* @ts-ignore */}
<Tooltip title="Rewind 10s" position="top">
<Tooltip anchorClassName='h-full' title={`Rewind ${currentInterval}s`} position="top">
<button
ref={arrowBackRef}
className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent"
@ -111,8 +111,6 @@ function PlayerControls(props: Props) {
<div className="p-1 border-l border-r bg-active-blue-border border-active-blue-border">
<OutsideClickDetectingDiv onClickOutside={handleClickOutside}>
<Popover
// open={showTooltip}
// interactive
// @ts-ignore
theme="nopadding"
animation="none"
@ -145,7 +143,7 @@ function PlayerControls(props: Props) {
>
<div onClick={toggleTooltip} ref={skipRef}>
{/* @ts-ignore */}
<Tooltip disabled={showTooltip} title="Set default skip duration">
<Tooltip anchorClassName='cursor-pointer' disabled={showTooltip} title="Set default skip duration">
{currentInterval}s
</Tooltip>
</div>
@ -153,7 +151,7 @@ function PlayerControls(props: Props) {
</OutsideClickDetectingDiv>
</div>
{/* @ts-ignore */}
<Tooltip title="Forward 10s" position="top">
<Tooltip anchorClassName='h-full' title={`Forward ${currentInterval}s`} position="top">
<button
ref={arrowForwardRef}
className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent"

View file

@ -1,169 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { sessions as sessionsRoute, assist as assistRoute, liveSession as liveSessionRoute, withSiteId } from 'App/routes';
import { Icon, BackLink, Link } from 'UI';
import { toggleFavorite, setSessionPath } from 'Duck/sessions';
import cn from 'classnames';
import { connectPlayer, toggleEvents } from 'Player';
import SessionMetaList from 'Shared/SessionItem/SessionMetaList';
import UserCard from './EventsBlock/UserCard';
import Tabs from 'Components/Session/Tabs';
import stl from './playerBlockHeader.module.css';
import AssistActions from '../Assist/components/AssistActions';
import AssistTabs from '../Assist/components/AssistTabs';
const SESSIONS_ROUTE = sessionsRoute();
const ASSIST_ROUTE = assistRoute();
@connectPlayer(
(state) => ({
width: state.width,
height: state.height,
live: state.live,
loading: state.cssLoading || state.messagesLoading,
showEvents: state.showEvents,
}),
{ toggleEvents }
)
@connect(
(state, props) => {
const isAssist = window.location.pathname.includes('/assist/');
const session = state.getIn(['sessions', 'current']);
return {
isAssist,
session,
sessionPath: state.getIn(['sessions', 'sessionPath']),
loading: state.getIn(['sessions', 'toggleFavoriteRequest', 'loading']),
disabled: state.getIn(['components', 'targetDefiner', 'inspectorMode']) || props.loading,
local: state.getIn(['sessions', 'timezone']),
funnelRef: state.getIn(['funnels', 'navRef']),
siteId: state.getIn(['site', 'siteId']),
metaList: state.getIn(['customFields', 'list']).map((i) => i.key),
closedLive: !!state.getIn(['sessions', 'errors']) || (isAssist && !session.live),
};
},
{
toggleFavorite,
setSessionPath,
}
)
@withRouter
export default class PlayerBlockHeader extends React.PureComponent {
state = {
hideBack: false,
};
componentDidMount() {
const { location } = this.props;
const queryParams = new URLSearchParams(location.search);
this.setState({ hideBack: queryParams.has('iframe') && queryParams.get('iframe') === 'true' });
}
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>
);
};
backHandler = () => {
const { history, siteId, sessionPath, isAssist } = this.props;
if (sessionPath.pathname === history.location.pathname || sessionPath.pathname.includes('/session/') || isAssist) {
history.push(withSiteId(isAssist ? ASSIST_ROUTE : SESSIONS_ROUTE, siteId));
} else {
history.push(sessionPath ? sessionPath.pathname + sessionPath.search : withSiteId(SESSIONS_ROUTE, siteId));
}
};
toggleFavorite = () => {
const { session } = this.props;
this.props.toggleFavorite(session.sessionId);
};
render() {
const {
width,
height,
session,
fullscreen,
metaList,
closedLive = false,
siteId,
isAssist,
setActiveTab,
activeTab,
showEvents,
toggleEvents,
} = this.props;
// const _live = isAssist;
const { hideBack } = this.state;
const { sessionId, userId, userNumericHash, live, 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 TABS = [this.props.tabs.EVENTS, this.props.tabs.HEATMAPS].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" onClick={this.backHandler}>
<BackLink label="Back" className="h-full" />
<div className={stl.divider} />
</div>
)}
<UserCard className="" width={width} height={height} />
{isAssist && <AssistTabs userId={userId} userNumericHash={userNumericHash} />}
<div className={cn('ml-auto flex items-center h-full', { hidden: closedLive })}>
{live && !isAssist && (
<>
<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>
)}
{isAssist && <AssistActions userId={userId} isCallActive={isCallActive} agentIds={agentIds} />}
</div>
</div>
{!isAssist && (
<div className="relative border-l" style={{ minWidth: '270px' }}>
<Tabs
tabs={TABS}
active={activeTab}
onClick={(tab) => {
if (activeTab === tab) {
setActiveTab('');
toggleEvents();
} else {
setActiveTab(tab);
!showEvents && toggleEvents(true);
}
}}
border={false}
/>
</div>
)}
</div>
);
}
}

View file

@ -0,0 +1,161 @@
import React from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import {
sessions as sessionsRoute,
assist as assistRoute,
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';
import AssistActions from '../Assist/components/AssistActions';
import AssistTabs from '../Assist/components/AssistTabs';
const SESSIONS_ROUTE = sessionsRoute();
const ASSIST_ROUTE = assistRoute();
// 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 toggleEvents = player.toggleEvents;
const {
session,
fullscreen,
metaList,
closedLive = false,
siteId,
isAssist,
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/') ||
isAssist
) {
history.push(withSiteId(isAssist ? ASSIST_ROUTE : SESSIONS_ROUTE, siteId));
} else {
history.push(
sessionPath ? sessionPath.pathname + sessionPath.search : withSiteId(SESSIONS_ROUTE, siteId)
);
}
};
const { sessionId, userId, userNumericHash, live, 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 TABS = [props.tabs.EVENTS, props.tabs.HEATMAPS].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" onClick={backHandler}>
{/* @ts-ignore TODO */}
<BackLink label="Back" className="h-full" />
<div className={stl.divider} />
</div>
)}
<UserCard className="" width={width} height={height} />
{isAssist && <AssistTabs userId={userId} userNumericHash={userNumericHash} />}
<div className={cn('ml-auto flex items-center h-full', { hidden: closedLive })}>
{live && !isAssist && (
<>
<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>
)}
{isAssist && (
// @ts-ignore TODO
<AssistActions userId={userId} isCallActive={isCallActive} agentIds={agentIds} />
)}
</div>
</div>
{!isAssist && (
<div className="relative border-l" style={{ minWidth: '270px' }}>
<Tabs
tabs={TABS}
active={activeTab}
onClick={(tab) => {
if (activeTab === tab) {
setActiveTab('');
toggleEvents();
} else {
setActiveTab(tab);
!showEvents && toggleEvents();
}
}}
border={false}
/>
</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']),
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),
closedLive: !!state.getIn(['sessions', 'errors']) || (isAssist && !session.live),
};
},
{
toggleFavorite,
setSessionPath,
}
)(observer(PlayerBlockHeader));
export default withRouter(PlayerHeaderCont);

View file

@ -2,8 +2,8 @@ import React from 'react';
import cn from 'classnames';
interface Props {
shades: Record<string, string>;
pathRoot: string;
shades?: Record<string, string>;
pathRoot?: string;
path: string;
diff: Record<string, any>;
}

View file

@ -1,321 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { hideHint } from 'Duck/components/player';
// import {
// connectPlayer,
// selectStorageType,
// STORAGE_TYPES,
// selectStorageListNow,
// selectStorageList,
// } from 'Player';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { JSONTree, NoContent, Tooltip } from 'UI';
import { formatMs } from 'App/date';
import { diff } from 'deep-diff';
import { jump } from 'Player';
import Autoscroll from '../Autoscroll';
import BottomBlock from '../BottomBlock/index';
import DiffRow from './DiffRow';
import cn from 'classnames';
import stl from './storage.module.css';
// const STATE = 'STATE';
// const DIFF = 'DIFF';
// const TABS = [ DIFF, STATE ].map(tab => ({ text: tab, key: tab }));
function getActionsName(type) {
switch (type) {
case STORAGE_TYPES.MOBX:
case STORAGE_TYPES.VUEX:
return 'MUTATIONS';
default:
return 'ACTIONS';
}
}
// @connectPlayer((state) => ({
// type: selectStorageType(state),
// list: selectStorageList(state),
// listNow: selectStorageListNow(state),
// }))
@connect(
(state) => ({
hintIsHidden: state.getIn(['components', 'player', 'hiddenHints', 'storage']),
}),
{
hideHint,
}
)
//@withEnumToggle('activeTab', 'setActiveTab', DIFF)
export default class Storage extends React.PureComponent {
lastBtnRef = React.createRef();
state = { showDiffs: false };
focusNextButton() {
if (this.lastBtnRef.current) {
this.lastBtnRef.current.focus();
}
}
componentDidMount() {
this.focusNextButton();
}
componentDidUpdate(prevProps) {
if (prevProps.listNow.length !== this.props.listNow.length) {
this.focusNextButton();
}
}
renderDiff(item, prevItem) {
if (!prevItem) {
// we don't have state before first action
return <div style={{ flex: 1 }} className="p-1" />;
}
const stateDiff = diff(prevItem.state, item.state);
if (!stateDiff) {
return (
<div style={{ flex: 3 }} className="flex flex-col p-2 pr-0 font-mono text-disabled-text">
No diff
</div>
);
}
return (
<div style={{ flex: 3 }} className="flex flex-col p-1 font-mono">
{stateDiff.map((d, i) => this.renderDiffs(d, i))}
</div>
);
}
renderDiffs(diff, i) {
const path = this.createPath(diff);
return (
<React.Fragment key={i}>
<DiffRow shades={this.pathShades} path={path} diff={diff} />
</React.Fragment>
);
}
createPath = (diff) => {
let path = [];
if (diff.path) {
path = path.concat(diff.path);
}
if (typeof diff.index !== 'undefined') {
path.push(diff.index);
}
const pathStr = path.length ? path.join('.') : '';
return pathStr;
};
ensureString(actionType) {
if (typeof actionType === 'string') return actionType;
return 'UNKNOWN';
}
goNext = () => {
const { list, listNow } = this.props;
jump(list[listNow.length].time, list[listNow.length]._index);
};
renderTab() {
const { listNow } = this.props;
if (listNow.length === 0) {
return 'Not initialized'; //?
}
return <JSONTree collapsed={2} src={listNow[listNow.length - 1].state} />;
}
renderItem(item, i, prevItem) {
const { type, listNow, list } = this.props;
let src;
let name;
switch (type) {
case STORAGE_TYPES.REDUX:
case STORAGE_TYPES.NGRX:
src = item.action;
name = src && src.type;
break;
case STORAGE_TYPES.VUEX:
src = item.mutation;
name = src && src.type;
break;
case STORAGE_TYPES.MOBX:
src = item.payload;
name = `@${item.type} ${src && src.type}`;
break;
case STORAGE_TYPES.ZUSTAND:
src = null;
name = item.mutation.join('');
}
if (src !== null && !this.state.showDiffs) {
this.setState({ showDiffs: true })
}
return (
<div
className={cn('flex justify-between items-start', src !== null ? 'border-b' : '')}
key={`store-${i}`}
>
{src === null ? (
<div className="font-mono" style={{ flex: 2, marginLeft: '26.5%' }}>
{name}
</div>
) : (
<>
{this.renderDiff(item, prevItem)}
<div style={{ flex: 2 }} className="flex pl-10 pt-2">
<JSONTree
name={this.ensureString(name)}
src={src}
collapsed
collapseStringsAfterLength={7}
/>
</div>
</>
)}
<div style={{ flex: 1 }} className="flex-1 flex gap-2 pt-2 items-center justify-end self-start">
{typeof item.duration === 'number' && (
<div className="font-size-12 color-gray-medium">{formatMs(item.duration)}</div>
)}
<div className="w-12">
{i + 1 < listNow.length && (
<button className={stl.button} onClick={() => jump(item.time, item._index)}>
{'JUMP'}
</button>
)}
{i + 1 === listNow.length && i + 1 < list.length && (
<button className={stl.button} ref={this.lastBtnRef} onClick={this.goNext}>
{'NEXT'}
</button>
)}
</div>
</div>
</div>
);
}
render() {
const { type, listNow, list, hintIsHidden } = this.props;
const showStore = type !== STORAGE_TYPES.MOBX;
return (
<BottomBlock>
<BottomBlock.Header>
{list.length > 0 && (
<div className="flex w-full">
{showStore && <h3 style={{ width: '25%', marginRight: 20 }} className="font-semibold">{'STATE'}</h3>}
{this.state.showDiffs ? (
<h3 style={{ width: '39%'}} className="font-semibold">
DIFFS
</h3>
) : null}
<h3 style={{ width: '30%' }} className="font-semibold">{getActionsName(type)}</h3>
<h3 style={{ paddingRight: 30, marginLeft: 'auto' }} className="font-semibold">
<Tooltip title="Time to execute">
TTE
</Tooltip>
</h3>
</div>
)}
</BottomBlock.Header>
<BottomBlock.Content className="flex">
<NoContent
title="Nothing to display yet."
subtext={
!hintIsHidden ? (
<>
{
'Inspect your application state while youre replaying your users sessions. OpenReplay supports '
}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/redux"
target="_blank"
>
Redux
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/vuex"
target="_blank"
>
VueX
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/pinia"
target="_blank"
>
Pinia
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/zustand"
target="_blank"
>
Zustand
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/mobx"
target="_blank"
>
MobX
</a>
{' and '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/ngrx"
target="_blank"
>
NgRx
</a>
.
<br />
<br />
<button className="color-teal" onClick={() => this.props.hideHint('storage')}>
Got It!
</button>
</>
) : null
}
size="small"
show={listNow.length === 0}
>
{showStore && (
<div className="ph-10 scroll-y" style={{ width: '25%' }}>
{listNow.length === 0 ? (
<div className="color-gray-light font-size-16 mt-20 text-center">
{'Empty state.'}
</div>
) : (
this.renderTab()
)}
</div>
)}
<div className="flex" style={{ width: showStore ? '75%' : '100%' }}>
<Autoscroll className="ph-10">
{listNow.map((item, i) =>
this.renderItem(item, i, i > 0 ? listNow[i - 1] : undefined)
)}
</Autoscroll>
</div>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
);
}
}

View file

@ -0,0 +1,315 @@
import React from 'react';
import { connect } from 'react-redux';
import { hideHint } from 'Duck/components/player';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { JSONTree, NoContent, Tooltip } from 'UI';
import { formatMs } from 'App/date';
import { diff } from 'deep-diff';
import { STORAGE_TYPES, selectStorageList, selectStorageListNow, selectStorageType } from 'Player';
import Autoscroll from '../Autoscroll';
import BottomBlock from '../BottomBlock/index';
import DiffRow from './DiffRow';
import cn from 'classnames';
import stl from './storage.module.css';
function getActionsName(type: string) {
switch (type) {
case STORAGE_TYPES.MOBX:
case STORAGE_TYPES.VUEX:
return 'MUTATIONS';
default:
return 'ACTIONS';
}
}
interface Props {
hideHint: (args: string) => void;
hintIsHidden: boolean;
}
function Storage(props: Props) {
const lastBtnRef = React.useRef<HTMLButtonElement>();
const [showDiffs, setShowDiffs] = React.useState(false);
const { player, store } = React.useContext(PlayerContext);
const state = store.get();
const listNow = selectStorageListNow(state);
const list = selectStorageList(state);
const type = selectStorageType(state);
const focusNextButton = () => {
if (lastBtnRef.current) {
lastBtnRef.current.focus();
}
};
React.useEffect(() => {
focusNextButton();
}, []);
React.useEffect(() => {
focusNextButton();
}, [listNow]);
const renderDiff = (item: Record<string, any>, prevItem: Record<string, any>) => {
if (!prevItem) {
// we don't have state before first action
return <div style={{ flex: 1 }} className="p-1" />;
}
const stateDiff = diff(prevItem.state, item.state);
if (!stateDiff) {
return (
<div style={{ flex: 3 }} className="flex flex-col p-2 pr-0 font-mono text-disabled-text">
No diff
</div>
);
}
return (
<div style={{ flex: 3 }} className="flex flex-col p-1 font-mono">
{stateDiff.map((d: Record<string, any>, i: number) => renderDiffs(d, i))}
</div>
);
};
const renderDiffs = (diff: Record<string, any>, i: number) => {
const path = createPath(diff);
return (
<React.Fragment key={i}>
<DiffRow path={path} diff={diff} />
</React.Fragment>
);
};
const createPath = (diff: Record<string, any>) => {
let path: string[] = [];
if (diff.path) {
path = path.concat(diff.path);
}
if (typeof diff.index !== 'undefined') {
path.push(diff.index);
}
const pathStr = path.length ? path.join('.') : '';
return pathStr;
};
const ensureString = (actionType: string) => {
if (typeof actionType === 'string') return actionType;
return 'UNKNOWN';
};
const goNext = () => {
// , list[listNow.length]._index
player.jump(list[listNow.length].time);
};
const renderTab = () => {
if (listNow.length === 0) {
return 'Not initialized'; //?
}
return <JSONTree collapsed={2} src={listNow[listNow.length - 1].state} />;
};
const renderItem = (item: Record<string, any>, i: number, prevItem: Record<string, any>) => {
let src;
let name;
switch (type) {
case STORAGE_TYPES.REDUX:
case STORAGE_TYPES.NGRX:
src = item.action;
name = src && src.type;
break;
case STORAGE_TYPES.VUEX:
src = item.mutation;
name = src && src.type;
break;
case STORAGE_TYPES.MOBX:
src = item.payload;
name = `@${item.type} ${src && src.type}`;
break;
case STORAGE_TYPES.ZUSTAND:
src = null;
name = item.mutation.join('');
}
if (src !== null && !showDiffs) {
setShowDiffs(true);
}
return (
<div
className={cn('flex justify-between items-start', src !== null ? 'border-b' : '')}
key={`store-${i}`}
>
{src === null ? (
<div className="font-mono" style={{ flex: 2, marginLeft: '26.5%' }}>
{name}
</div>
) : (
<>
{renderDiff(item, prevItem)}
<div style={{ flex: 2 }} className="flex pl-10 pt-2">
<JSONTree
name={ensureString(name)}
src={src}
collapsed
collapseStringsAfterLength={7}
/>
</div>
</>
)}
<div
style={{ flex: 1 }}
className="flex-1 flex gap-2 pt-2 items-center justify-end self-start"
>
{typeof item.duration === 'number' && (
<div className="font-size-12 color-gray-medium">{formatMs(item.duration)}</div>
)}
<div className="w-12">
{i + 1 < listNow.length && (
<button className={stl.button} onClick={() => jump(item.time, item._index)}>
{'JUMP'}
</button>
)}
{i + 1 === listNow.length && i + 1 < list.length && (
<button className={stl.button} ref={lastBtnRef} onClick={goNext}>
{'NEXT'}
</button>
)}
</div>
</div>
</div>
);
};
const { hintIsHidden } = props;
const showStore = type !== STORAGE_TYPES.MOBX;
return (
<BottomBlock>
<BottomBlock.Header>
{list.length > 0 && (
<div className="flex w-full">
{showStore && (
<h3 style={{ width: '25%', marginRight: 20 }} className="font-semibold">
{'STATE'}
</h3>
)}
{showDiffs ? (
<h3 style={{ width: '39%' }} className="font-semibold">
DIFFS
</h3>
) : null}
<h3 style={{ width: '30%' }} className="font-semibold">
{getActionsName(type)}
</h3>
<h3 style={{ paddingRight: 30, marginLeft: 'auto' }} className="font-semibold">
<Tooltip title="Time to execute">TTE</Tooltip>
</h3>
</div>
)}
</BottomBlock.Header>
<BottomBlock.Content className="flex">
<NoContent
title="Nothing to display yet."
subtext={
!hintIsHidden ? (
<>
{
'Inspect your application state while youre replaying your users sessions. OpenReplay supports '
}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/redux"
target="_blank"
>
Redux
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/vuex"
target="_blank"
>
VueX
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/pinia"
target="_blank"
>
Pinia
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/zustand"
target="_blank"
>
Zustand
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/mobx"
target="_blank"
>
MobX
</a>
{' and '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/ngrx"
target="_blank"
>
NgRx
</a>
.
<br />
<br />
<button className="color-teal" onClick={() => props.hideHint('storage')}>
Got It!
</button>
</>
) : null
}
size="small"
show={listNow.length === 0}
>
{showStore && (
<div className="ph-10 scroll-y" style={{ width: '25%' }}>
{listNow.length === 0 ? (
<div className="color-gray-light font-size-16 mt-20 text-center">
{'Empty state.'}
</div>
) : (
renderTab()
)}
</div>
)}
<div className="flex" style={{ width: showStore ? '75%' : '100%' }}>
<Autoscroll className="ph-10">
{listNow.map((item: Record<string, any>, i: number) =>
renderItem(item, i, i > 0 ? listNow[i - 1] : undefined)
)}
</Autoscroll>
</div>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
);
}
export default connect(
(state: any) => ({
hintIsHidden: state.getIn(['components', 'player', 'hiddenHints', 'storage']),
}),
{
hideHint,
}
)(observer(Storage));

View file

@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { setCreateNoteTooltip } from 'Duck/sessions';
import GuidePopup from 'Shared/GuidePopup';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
function NotePopup({
setCreateNoteTooltip,
@ -14,12 +13,11 @@ function NotePopup({
tooltipActive: boolean;
}) {
const { player, store } = React.useContext(PlayerContext)
const { time } = store.get();
const toggleNotePopup = () => {
if (tooltipActive) return;
player.pause();
setCreateNoteTooltip({ time: time, isVisible: true });
setCreateNoteTooltip({ time: store.get().time, isVisible: true });
};
React.useEffect(() => {
@ -42,11 +40,9 @@ function NotePopup({
);
}
const NotePopupPl = observer(NotePopup);
const NotePopupComp = connect(
(state: any) => ({ tooltipActive: state.getIn(['sessions', 'createNoteTooltip', 'isVisible']) }),
{ setCreateNoteTooltip }
)(NotePopupPl);
)(NotePopup);
export default React.memo(NotePopupComp);

View file

@ -69,6 +69,8 @@ export default function GuidePopup({ children, title, description }: IProps) {
</Tooltip>
</div>
) : (
children
<>
{children}
</>
);
}

View file

@ -4,7 +4,7 @@ import type { Placement } from '@floating-ui/react-dom-interactions';
import cn from 'classnames';
interface Props {
title?: any;
title: React.ReactNode;
children: any;
disabled?: boolean;
open?: boolean;
@ -13,6 +13,7 @@ interface Props {
delay?: number;
style?: any;
offset?: number;
anchorClassName?: string;
}
function Tooltip(props: Props) {
const {
@ -21,6 +22,7 @@ function Tooltip(props: Props) {
open = false,
placement,
className = '',
anchorClassName = '',
delay = 500,
style = {},
offset = 5,
@ -38,7 +40,7 @@ function Tooltip(props: Props) {
return (
<div className="relative">
<TooltipAnchor state={state}>{props.children}</TooltipAnchor>
<TooltipAnchor className={anchorClassName} state={state}>{props.children}</TooltipAnchor>
<FloatingTooltip
state={state}
className={cn('bg-gray-darkest color-white rounded py-1 px-2 animate-fade', className)}

View file

@ -168,6 +168,26 @@ export default class Animator {
}
}
jumpInterval(interval: number) {
const { endTime, time } = this.store.get()
if (interval > 0) {
return this.jump(
Math.min(
endTime,
time + interval
)
);
} else {
return this.jump(
Math.max(
0,
time - interval
)
);
}
}
// TODO: clearify logic of live time-travel
jumpToLive() {
cancelAnimationFrame(this.animationFrameRequestId)
@ -177,4 +197,4 @@ export default class Animator {
}
}
}

View file

@ -16,7 +16,7 @@ export default class InspectorController {
}
}
enableInspector(clickCallback: (e: { target: Element }) => void): Document | null {
enableInspector(clickCallback?: (e: { target: Element }) => void): Document | null {
const parent = this.screen.getParentElement()
if (!parent) return null;
if (!this.substitutor) {
@ -28,7 +28,7 @@ export default class InspectorController {
}
this.substitutor.display(false)
const docElement = this.screen.document?.documentElement // this.substitutor.document?.importNode(
const doc = this.substitutor.document
if (doc && docElement) {
@ -43,11 +43,11 @@ export default class InspectorController {
this.substitutor.display(true);
return doc;
}
disableInspector() {
if (this.substitutor) {
const doc = this.substitutor.document;
if (doc) {
if (doc) {
doc.documentElement.innerHTML = "";
}
this.inspector.clean();
@ -56,4 +56,4 @@ export default class InspectorController {
this.screen.display(true);
}
}
}

View file

@ -1,14 +1,14 @@
import type Screen from './Screen'
import type Marker from './Marker'
//import { select } from 'optimal-select';
//import { select } from 'optimal-select';
export default class Inspector {
// private captureCallbacks = [];
// private bubblingCallbacks = [];
constructor(private screen: Screen, private marker: Marker) {}
private onMouseMove = (e: MouseEvent) => {
private onMouseMove = (e: MouseEvent) => {
// const { overlay } = this.screen;
// if (!overlay.contains(e.target)) {
// return;
@ -21,7 +21,7 @@ export default class Inspector {
return;
}
this.marker.mark(target);
this.marker.mark(target);
}
private onOverlayLeave = () => {
@ -57,7 +57,7 @@ export default class Inspector {
// }
private clickCallback: (e: { target: Element }) => void | null = null
enable(clickCallback: Inspector['clickCallback']) {
enable(clickCallback?: Inspector['clickCallback']) {
this.screen.overlay.addEventListener('mousemove', this.onMouseMove)
this.screen.overlay.addEventListener('mouseleave', this.onOverlayLeave)
this.screen.overlay.addEventListener('click', this.onMarkClick)
@ -67,4 +67,4 @@ export default class Inspector {
this.screen.overlay.removeEventListener('mouseleave', this.onOverlayLeave)
this.screen.overlay.removeEventListener('click', this.onMarkClick)
}
}
}

View file

@ -4,7 +4,7 @@ import Player, { State as PlayerState } from '../player/Player'
import MessageManager from './MessageManager'
import InspectorController from './InspectorController'
import TargetMarker from './TargetMarker'
import AssistManager, {
import AssistManager, {
INITIAL_STATE as ASSIST_INITIAL_STATE,
} from './assist/AssistManager'
import Screen from './Screen/Screen'
@ -40,7 +40,7 @@ export default class WebPlayer extends Player {
} : {}
const screen = new Screen()
const messageManager = new MessageManager(session, wpState, screen, initialLists)
const messageManager = new MessageManager(session, wpState, screen, initialLists)
super(wpState, messageManager)
this.screen = screen
this.messageManager = messageManager
@ -48,14 +48,14 @@ export default class WebPlayer extends Player {
this.targetMarker = new TargetMarker(this.screen, wpState)
this.inspectorController = new InspectorController(screen)
const endTime = !live && session.duration.valueOf()
wpState.update({
//@ts-ignore
initialized: true,
//@ts-ignore
session,
live,
livePlay: live,
endTime, // : 0, //TODO: through initialState
@ -85,7 +85,7 @@ export default class WebPlayer extends Player {
mark(e: Element) {
this.inspectorController.marker?.mark(e)
}
toggleInspectorMode(flag: boolean, clickCallback) {
toggleInspectorMode(flag: boolean, clickCallback?: (args: any) => any) {
if (typeof flag !== 'boolean') {
const { inspectorMode } = this.wpState.get()
flag = !inspectorMode;
@ -104,6 +104,7 @@ export default class WebPlayer extends Player {
setActiveTarget(args: Parameters<TargetMarker['setActiveTarget']>) {
this.targetMarker.setActiveTarget(...args)
}
markTargets(args: Parameters<TargetMarker['markTargets']>) {
this.pause()
this.targetMarker.markTargets(...args)
@ -133,11 +134,10 @@ export default class WebPlayer extends Player {
toggleUserName(name?: string) {
this.screen.cursor.showTag(name)
}
clean() {
super.clean()
this.assistManager.clean()
window.removeEventListener('resize', this.scale)
}
}

View file

@ -461,7 +461,7 @@ export default class AssistManager {
onStream: (s: MediaStream)=>void,
onCallEnd: () => void,
onReject: () => void,
onError?: ()=> void,
onError?: (e?: any)=> void,
) {
this.callArgs = {
localStream,
@ -472,7 +472,7 @@ export default class AssistManager {
}
}
public call(thirdPartyPeers?: string[]): { end: Function } {
public call(thirdPartyPeers?: string[]): { end: () => void } {
if (thirdPartyPeers && thirdPartyPeers.length > 0) {
this.addPeerCall(thirdPartyPeers)
} else {