refactor(ui/player): player parts decomposition, remove redux, fix performance issues, remove class compoents from player core

Player refactoring phase 1
This commit is contained in:
Delirium 2022-11-29 16:09:49 +01:00 committed by GitHub
commit 5a4480599f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
175 changed files with 4172 additions and 4344 deletions

87
.github/workflows/frontend-dev.yaml vendored Normal file
View file

@ -0,0 +1,87 @@
name: Frontend Dev Deployment
on:
workflow_dispatch:
push:
branches:
- player-refactoring-phase-1
paths:
- frontend/**
# Disable previous workflows for this action.
concurrency:
group: ${{ github.workflow }} #-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Cache node modules
uses: actions/cache@v1
with:
path: node_modules
key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.OS }}-build-
${{ runner.OS }}-
- name: Docker login
run: |
docker login ${{ secrets.OSS_REGISTRY_URL }} -u ${{ secrets.OSS_DOCKER_USERNAME }} -p "${{ secrets.OSS_REGISTRY_TOKEN }}"
- uses: azure/k8s-set-context@v1
with:
method: kubeconfig
kubeconfig: ${{ secrets.DEV_KUBECONFIG }} # Use content of kubeconfig in secret.
id: setcontext
- name: Building and Pushing frontend image
id: build-image
env:
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
ENVIRONMENT: staging
run: |
set -x
cd frontend
mv .env.sample .env
docker run --rm -v /etc/passwd:/etc/passwd -u `id -u`:`id -g` -v $(pwd):/home/${USER} -w /home/${USER} --name node_build node:14-stretch-slim /bin/bash -c "yarn && yarn build"
# https://github.com/docker/cli/issues/1134#issuecomment-613516912
DOCKER_BUILDKIT=1 docker build --target=cicd -t $DOCKER_REPO/frontend:${IMAGE_TAG} .
docker tag $DOCKER_REPO/frontend:${IMAGE_TAG} $DOCKER_REPO/frontend:${IMAGE_TAG}-ee
docker push $DOCKER_REPO/frontend:${IMAGE_TAG}
docker push $DOCKER_REPO/frontend:${IMAGE_TAG}-ee
- name: Deploy to kubernetes foss
run: |
cd scripts/helmcharts/
set -x
cat <<EOF>>/tmp/image_override.yaml
frontend:
image:
tag: ${IMAGE_TAG}
EOF
## Update secerts
sed -i "s#openReplayContainerRegistry.*#openReplayContainerRegistry: \"${{ secrets.OSS_REGISTRY_URL }}\"#g" vars.yaml
sed -i "s/postgresqlPassword: \"changeMePassword\"/postgresqlPassword: \"${{ secrets.DEV_PG_PASSWORD }}\"/g" vars.yaml
sed -i "s/accessKey: \"changeMeMinioAccessKey\"/accessKey: \"${{ secrets.DEV_MINIO_ACCESS_KEY }}\"/g" vars.yaml
sed -i "s/secretKey: \"changeMeMinioPassword\"/secretKey: \"${{ secrets.DEV_MINIO_SECRET_KEY }}\"/g" vars.yaml
sed -i "s/jwt_secret: \"SetARandomStringHere\"/jwt_secret: \"${{ secrets.DEV_JWT_SECRET }}\"/g" vars.yaml
sed -i "s/domainName: \"\"/domainName: \"${{ secrets.DEV_DOMAIN_NAME }}\"/g" vars.yaml
# Update changed image tag
sed -i "/frontend/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
cat /tmp/image_override.yaml
# Deploy command
mv openreplay/charts/{ingress-nginx,frontend,quickwit} /tmp
rm -rf openreplay/charts/*
mv /tmp/{ingress-nginx,frontend,quickwit} openreplay/charts/
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
env:
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}

View file

@ -1,4 +1,4 @@
name: Frontend FOSS Deployment
name: Frontend Dev Deployment
on:
workflow_dispatch:
push:

View file

@ -2,13 +2,10 @@ import { configure, addDecorator } from '@storybook/react';
import { Provider } from 'react-redux';
import store from '../app/store';
import { MemoryRouter } from "react-router"
import { PlayerProvider } from '../app/player/store'
const withProvider = (story) => (
<Provider store={store}>
<PlayerProvider>
{ story() }
</PlayerProvider>
</Provider>
)
@ -33,4 +30,4 @@ configure(
require.context('../app', true, /\.stories\.js$/),
],
module
);
);

View file

@ -6,7 +6,7 @@ import stl from './chatWindow.module.css';
import ChatControls from '../ChatControls/ChatControls';
import Draggable from 'react-draggable';
import type { LocalStream } from 'Player';
import { toggleVideoLocalStream } from 'Player'
import { PlayerContext } from 'App/components/Session/playerContext';
export interface Props {
incomeStream: MediaStream[] | null;
@ -17,6 +17,10 @@ export interface Props {
}
function ChatWindow({ userId, incomeStream, localStream, endCall, isPrestart }: Props) {
const { player } = React.useContext(PlayerContext)
const toggleVideoLocalStream = player.assistManager.toggleVideoLocalStream;
const [localVideoEnabled, setLocalVideoEnabled] = useState(false);
const [anyRemoteEnabled, setRemoteEnabled] = useState(false);

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

@ -25,7 +25,7 @@ const sortOptions = Object.entries(sortOptionsMap)
@connect(state => ({
loading: state.getIn([ "errors", "loading" ]),
resolveToggleLoading: state.getIn(["errors", "resolve", "loading"]) ||
resolveToggleLoading: state.getIn(["errors", "resolve", "loading"]) ||
state.getIn(["errors", "unresolve", "loading"]),
ignoreLoading: state.getIn([ "errors", "ignore", "loading" ]),
mergeLoading: state.getIn([ "errors", "merge", "loading" ]),
@ -54,19 +54,19 @@ export default class List extends React.PureComponent {
}
this.debounceFetch = debounce(this.props.editOptions, 1000);
}
componentDidMount() {
this.props.applyFilter({ });
}
check = ({ errorId }) => {
const { checkedIds } = this.state;
const newCheckedIds = checkedIds.contains(errorId)
? checkedIds.remove(errorId)
const newCheckedIds = checkedIds.contains(errorId)
? checkedIds.remove(errorId)
: checkedIds.add(errorId);
this.setState({
checkedAll: newCheckedIds.size === this.props.list.size,
checkedIds: newCheckedIds
checkedIds: newCheckedIds
});
}
@ -184,7 +184,7 @@ export default class List extends React.PureComponent {
onClick={ this.unresolve }
disabled={ someLoading || currentCheckedIds.size === 0}
/>
}
}
{ status !== IGNORED &&
<IconButton
outline
@ -196,10 +196,10 @@ export default class List extends React.PureComponent {
onClick={ this.ignore }
disabled={ someLoading || currentCheckedIds.size === 0}
/>
}
}
</div>
<div className="flex items-center ml-6">
<span className="mr-2 color-gray-medium">Sort By</span>
<span className="mr-2 color-gray-medium">Sort By</span>
<Select
defaultValue={ `${sort}-${order}` }
name="sort"
@ -212,7 +212,6 @@ export default class List extends React.PureComponent {
wrapperClassName="ml-3"
placeholder="Filter by name or message"
icon="search"
iconPosition="left"
name="filter"
onChange={ this.onQueryChange }
value={query}
@ -258,4 +257,4 @@ export default class List extends React.PureComponent {
</div>
);
}
}
}

View file

@ -13,7 +13,7 @@ function Crashes({ player }) {
const [ filter, setFilter ] = useState("");
const filterRE = getRE(filter, 'i');
const filtered = player.lists[CRASHES].listNow.filter(({ name, reason, stacktrace }) =>
const filtered = player.lists[CRASHES].listNow.filter(({ name, reason, stacktrace }) =>
filterRE.test(name) || filterRE.test(reason) || filterRE.test(stacktrace)
);
return (
@ -23,7 +23,6 @@ function Crashes({ player }) {
className="input-small"
placeholder="Filter"
icon="search"
iconPosition="left"
name="filter"
onChange={ setFilter }
/>

View file

@ -20,13 +20,13 @@ function Logs({ player }) {
const [ activeTab, setTab ] = useState(ALL);
const onInputChange = useCallback(({ target }) => setFilter(target.value));
const filterRE = getRE(filter, 'i');
const filtered = player.lists[LOGS].listNow.filter(({ severity, content }) =>
const filtered = player.lists[LOGS].listNow.filter(({ severity, content }) =>
(activeTab === ALL || activeTab === severity) && filterRE.test(content)
);
return (
<>
<PanelLayout.Header>
<Tabs
<Tabs
tabs={ TABS }
active={ activeTab }
onClick={ setTab }
@ -36,7 +36,6 @@ function Logs({ player }) {
className="input-small"
placeholder="Filter"
icon="search"
iconPosition="left"
name="filter"
onChange={ onInputChange }
/>
@ -48,8 +47,8 @@ function Logs({ player }) {
title="No recordings found"
>
<Autoscroll>
{ filtered.map(log =>
<Log text={log.content} level={log.severity}/>
{ filtered.map(log =>
<Log text={log.content} level={log.severity}/>
)}
</Autoscroll>
</NoContent>

View file

@ -2,9 +2,8 @@ import React from 'react';
import { connect } from 'react-redux';
import { useState } from 'react';
import { NoContent, Tabs } from 'UI';
import withEnumToggle from 'HOCs/withEnumToggle';
import { hideHint } from 'Duck/components/player';
import { typeList } from 'Types/session/stackEvent';
import { typeList } from 'Types/session/stackEvent';
import * as PanelLayout from './PanelLayout';
import UserEvent from 'Components/Session_/StackEvents/UserEvent';
@ -14,7 +13,7 @@ const ALL = 'ALL';
const TABS = [ ALL, ...typeList ].map(tab =>({ text: tab, key: tab }));
function StackEvents({
function StackEvents({
stackEvents,
hintIsHidden,
hideHint,
@ -28,10 +27,10 @@ function StackEvents({
return (
<>
<PanelLayout.Header>
<Tabs
<Tabs
className="uppercase"
tabs={ tabs }
active={ activeTab }
active={ activeTab }
onClick={ setTab }
border={ false }
/>
@ -39,12 +38,12 @@ function StackEvents({
<PanelLayout.Body>
<NoContent
title="Nothing to display yet."
subtext={ !hintIsHidden
?
subtext={ !hintIsHidden
?
<>
<a className="underline color-teal" href="https://docs.openreplay.com/integrations" target="_blank">Integrations</a>
{' and '}
<a className="underline color-teal" href="https://docs.openreplay.com/api#event" target="_blank">Events</a>
<a className="underline color-teal" href="https://docs.openreplay.com/api#event" target="_blank">Events</a>
{ ' make debugging easier. Sync your backend logs and custom events with session replay.' }
<br/><br/>
<button className="color-teal" onClick={() => hideHint("stack")}>Got It!</button>
@ -66,8 +65,8 @@ function StackEvents({
}
export default connect(state => ({
hintIsHidden: state.getIn(['components', 'player', 'hiddenHints', 'stack']) ||
hintIsHidden: state.getIn(['components', 'player', 'hiddenHints', 'stack']) ||
!state.getIn([ 'site', 'list' ]).some(s => s.stackIntegrations),
}), {
hideHint
})(StackEvents);
})(StackEvents);

View file

@ -1,21 +1,17 @@
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 { 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({
function LivePlayer ({
session,
toggleFullscreen,
closeBottomBlock,
@ -27,6 +23,7 @@ function LivePlayer({
userEmail,
userName,
}) {
const [contextValue, setContextValue] = useState<IPlayerContext>(defaultContextValue);
const [fullView, setFullView] = useState(false);
useEffect(() => {
if (!loadingCredentials) {
@ -36,11 +33,18 @@ function LivePlayer({
email: userEmail,
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();
}, [session.sessionId, loadingCredentials, assistCredendials]);
return () => LivePlayer.clean()
}, [ session.sessionId, loadingCredentials, assistCredendials ]);
// LAYOUT (TODO: local layout state - useContext or something..)
useEffect(() => {
@ -64,22 +68,15 @@ function LivePlayer({
};
const [activeTab, setActiveTab] = useState('');
if (!contextValue.player) return null;
return (
<PlayerProvider>
<InitLoader className="flex-1 p-3">
{!fullView && (
<PlayerBlockHeader
activeTab={activeTab}
setActiveTab={setActiveTab}
tabs={TABS}
fullscreen={fullscreen}
/>
)}
<div className={styles.session} data-fullscreen={fullscreen || fullView}>
<PlayerBlock fullView={fullView} />
</div>
</InitLoader>
</PlayerProvider>
<PlayerContext.Provider value={contextValue}>
{!fullView && (<PlayerBlockHeader activeTab={activeTab} setActiveTab={setActiveTab} tabs={TABS} fullscreen={fullscreen}/>)}
<div className={ styles.session } data-fullscreen={fullscreen}>
<PlayerBlock />
</div>
</PlayerContext.Provider>
);
}

View file

@ -0,0 +1,79 @@
import React from 'react';
import PlayerBlock from '../Session_/PlayerBlock';
import styles from '../Session_/session.module.css';
import { countDaysFrom } from 'App/date';
import cn from 'classnames';
import RightBlock from './RightBlock';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
const TABS = {
EVENTS: 'User Steps',
HEATMAPS: 'Click Map',
};
function PlayerContent({ session, live, fullscreen, activeTab, setActiveTab }) {
const { store } = React.useContext(PlayerContext)
const {
error,
} = store.get()
const hasError = !!error
const sessionDays = countDaysFrom(session.startedAt);
return (
<div className="relative">
{hasError ? (
<div
className="inset-0 flex items-center justify-center absolute"
style={{
// background: '#f6f6f6',
height: 'calc(100vh - 50px)',
zIndex: '999',
}}
>
<div className="flex flex-col items-center">
<div className="text-lg -mt-8">
{sessionDays > 2 ? 'Session not found.' : 'This session is still being processed.'}
</div>
<div className="text-sm">
{sessionDays > 2
? 'Please check your data retention policy.'
: 'Please check it again in a few minutes.'}
</div>
</div>
</div>
) : (
<div className={cn('flex', { 'pointer-events-none': hasError })}>
<div
className="w-full"
style={activeTab && !fullscreen ? { maxWidth: 'calc(100% - 270px)' } : undefined}
>
<div className={cn(styles.session, 'relative')} data-fullscreen={fullscreen}>
<PlayerBlock activeTab={activeTab} />
</div>
</div>
{activeTab !== '' && (
<RightMenu
activeTab={activeTab}
setActiveTab={setActiveTab}
fullscreen={fullscreen}
tabs={TABS}
live={live}
/>
)}
</div>
)}
</div>
);
}
function RightMenu({ live, tabs, activeTab, setActiveTab, fullscreen }) {
return (
!live &&
!fullscreen && <RightBlock tabs={tabs} setActiveTab={setActiveTab} activeTab={activeTab} />
);
}
export default observer(PlayerContent);

View file

@ -1,23 +1,24 @@
import React, { useState } from 'react'
import React from 'react'
import EventsBlock from '../Session_/EventsBlock';
import PageInsightsPanel from '../Session_/PageInsightsPanel/PageInsightsPanel'
import { Controls as PlayerControls } from 'Player';
import { connectPlayer } from 'Player';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import cn from 'classnames';
import stl from './rightblock.module.css';
const EventsBlockConnected = connectPlayer(state => ({
currentTimeEventIndex: state.eventListNow.length > 0 ? state.eventListNow.length - 1 : 0,
playing: state.playing,
}))(EventsBlock)
function RightBlock(props) {
function RightBlock(props: any) {
const { activeTab } = props;
const { player, store } = React.useContext(PlayerContext)
const renderActiveTab = (tab) => {
const { eventListNow, playing } = store.get()
const currentTimeEventIndex = eventListNow.length > 0 ? eventListNow.length - 1 : 0
const EventsBlockConnected = () => <EventsBlock playing={playing} player={player} setActiveTab={props.setActiveTab} currentTimeEventIndex={currentTimeEventIndex} />
const renderActiveTab = (tab: string) => {
switch(tab) {
case props.tabs.EVENTS:
return <EventsBlockConnected setActiveTab={props.setActiveTab} player={PlayerControls}/>
return <EventsBlockConnected />
case props.tabs.HEATMAPS:
return <PageInsightsPanel setActiveTab={props.setActiveTab} />
}
@ -29,4 +30,4 @@ function RightBlock(props) {
)
}
export default React.memo(RightBlock)
export default observer(RightBlock)

View file

@ -1,139 +0,0 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { Loader, Modal } from 'UI';
import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player';
import { fetchList } from 'Duck/integrations';
import { PlayerProvider, injectNotes, connectPlayer, init as initPlayer, clean as cleanPlayer, Controls } from 'Player';
import cn from 'classnames';
import RightBlock from './RightBlock';
import withLocationHandlers from 'HOCs/withLocationHandlers';
import { useStore } from 'App/mstore'
import PlayerBlockHeader from '../Session_/PlayerBlockHeader';
import PlayerBlock from '../Session_/PlayerBlock';
import styles from '../Session_/session.module.css';
import { countDaysFrom } from 'App/date';
import ReadNote from '../Session_/Player/Controls/components/ReadNote';
import { fetchList as fetchMembers } from 'Duck/member';
const TABS = {
EVENTS: 'User Steps',
HEATMAPS: 'Click Map',
};
const InitLoader = connectPlayer((state) => ({
loading: !state.initialized,
}))(Loader);
const PlayerContentConnected = connectPlayer((state) => ({
showEvents: !state.showEvents,
hasError: state.error,
}))(PlayerContent);
function PlayerContent({ session, live, fullscreen, activeTab, setActiveTab, hasError }) {
const sessionDays = countDaysFrom(session.startedAt);
return (
<div className="relative">
{hasError ? (
<div className="inset-0 flex items-center justify-center absolute" style={{
// background: '#f6f6f6',
height: 'calc(100vh - 50px)',
zIndex: '999',
}}>
<div className="flex flex-col items-center">
<div className="text-lg -mt-8">{sessionDays > 2 ? 'Session not found.' : 'This session is still being processed.'}</div>
<div className="text-sm">{sessionDays > 2 ? 'Please check your data retention policy.' : 'Please check it again in a few minutes.'}</div>
</div>
</div>
) : (
<div className={cn('flex', { 'pointer-events-none': hasError })}>
<div className="w-full" style={activeTab && !fullscreen ? { maxWidth: 'calc(100% - 270px)'} : undefined}>
<div className={cn(styles.session, 'relative')} data-fullscreen={fullscreen}>
<PlayerBlock activeTab={activeTab} />
</div>
</div>
{activeTab !== '' && <RightMenu activeTab={activeTab} setActiveTab={setActiveTab} fullscreen={fullscreen} tabs={TABS} live={live} />}
</div>
)}
</div>
);
}
function RightMenu({ live, tabs, activeTab, setActiveTab, fullscreen }) {
return !live && !fullscreen && <RightBlock tabs={tabs} setActiveTab={setActiveTab} activeTab={activeTab} />;
}
function WebPlayer(props) {
const { session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt, fetchList } = props;
const { notesStore } = useStore()
const [activeTab, setActiveTab] = useState('');
const [showNoteModal, setShowNote] = useState(false)
const [noteItem, setNoteItem] = useState(null)
useEffect(() => {
fetchList('issues');
initPlayer(session, jwt);
props.fetchMembers()
notesStore.fetchSessionNotes(session.sessionId).then(r => {
injectNotes(r)
const note = props.query.get('note');
if (note) {
Controls.pause()
setNoteItem(notesStore.getNoteById(parseInt(note, 10), r))
setShowNote(true)
}
})
const jumptTime = props.query.get('jumpto');
if (jumptTime) {
Controls.jump(parseInt(jumptTime));
}
return () => cleanPlayer();
}, [session.sessionId]);
// LAYOUT (TODO: local layout state - useContext or something..)
useEffect(
() => () => {
toggleFullscreen(false);
closeBottomBlock();
},
[]
);
const onNoteClose = () => {setShowNote(false); Controls.togglePlay()}
return (
<PlayerProvider>
<InitLoader className="flex-1">
<PlayerBlockHeader activeTab={activeTab} setActiveTab={setActiveTab} tabs={TABS} fullscreen={fullscreen} />
<PlayerContentConnected activeTab={activeTab} fullscreen={fullscreen} live={live} setActiveTab={setActiveTab} session={session} />
<Modal open={showNoteModal} onClose={onNoteClose}>
{showNoteModal ? (
<ReadNote
userEmail={props.members.find(m => m.id === noteItem?.userId)?.email || ''}
note={noteItem}
onClose={onNoteClose}
notFound={!noteItem}
/>
) : null}
</Modal>
</InitLoader>
</PlayerProvider>
);
}
export default connect(
(state) => ({
session: state.getIn(['sessions', 'current']),
jwt: state.get('jwt'),
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
showEvents: state.get('showEvents'),
members: state.getIn(['members', 'list']),
}),
{
toggleFullscreen,
closeBottomBlock,
fetchList,
fetchMembers,
}
)(withLocationHandlers()(WebPlayer));

View file

@ -0,0 +1,129 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { Modal } from 'UI';
import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player';
import { fetchList } from 'Duck/integrations';
import { createWebPlayer } from 'Player';
import { makeAutoObservable } from 'mobx';
import withLocationHandlers from 'HOCs/withLocationHandlers';
import { useStore } from 'App/mstore';
import PlayerBlockHeader from '../Session_/PlayerBlockHeader';
import ReadNote from '../Session_/Player/Controls/components/ReadNote';
import { fetchList as fetchMembers } from 'Duck/member';
import PlayerContent from './PlayerContent';
import { IPlayerContext, PlayerContext, defaultContextValue } from './playerContext';
const TABS = {
EVENTS: 'User Steps',
HEATMAPS: 'Click Map',
};
function WebPlayer(props: any) {
const {
session,
toggleFullscreen,
closeBottomBlock,
live,
fullscreen,
jwt,
fetchList
} = props;
const { notesStore } = useStore();
const [activeTab, setActiveTab] = useState('');
const [showNoteModal, setShowNote] = useState(false);
const [noteItem, setNoteItem] = useState(null);
const [contextValue, setContextValue] = useState<IPlayerContext>(defaultContextValue);
useEffect(() => {
fetchList('issues');
const [WebPlayerInst, PlayerStore] = createWebPlayer(session, (state) =>
makeAutoObservable(state)
);
setContextValue({ player: WebPlayerInst, store: PlayerStore });
props.fetchMembers();
notesStore.fetchSessionNotes(session.sessionId).then((r) => {
const note = props.query.get('note');
if (note) {
WebPlayerInst.pause();
setNoteItem(notesStore.getNoteById(parseInt(note, 10), r));
setShowNote(true);
}
});
const jumptTime = props.query.get('jumpto');
if (jumptTime) {
WebPlayerInst.jump(parseInt(jumptTime));
}
return () => WebPlayerInst.clean();
}, [session.sessionId]);
// LAYOUT (TODO: local layout state - useContext or something..)
useEffect(
() => () => {
toggleFullscreen(false);
closeBottomBlock();
},
[]
);
const onNoteClose = () => {
setShowNote(false);
contextValue.player.togglePlay();
};
if (!contextValue.player) return null;
return (
<PlayerContext.Provider value={contextValue}>
<>
<PlayerBlockHeader
// @ts-ignore TODO?
activeTab={activeTab}
setActiveTab={setActiveTab}
tabs={TABS}
fullscreen={fullscreen}
/>
{/* @ts-ignore */}
<PlayerContent
activeTab={activeTab}
fullscreen={fullscreen}
live={live}
setActiveTab={setActiveTab}
session={session}
/>
<Modal open={showNoteModal} onClose={onNoteClose}>
{showNoteModal ? (
<ReadNote
userEmail={
props.members.find((m: Record<string, any>) => m.id === noteItem?.userId)
?.email || ''
}
note={noteItem}
onClose={onNoteClose}
notFound={!noteItem}
/>
) : null}
</Modal>
</>
</PlayerContext.Provider>
);
}
export default connect(
(state: any) => ({
session: state.getIn(['sessions', 'current']),
jwt: state.get('jwt'),
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
showEvents: state.get('showEvents'),
members: state.getIn(['members', 'list']),
}),
{
toggleFullscreen,
closeBottomBlock,
fetchList,
fetchMembers,
}
)(withLocationHandlers()(WebPlayer));

View file

@ -0,0 +1,12 @@
import { createContext } from 'react';
import {
IWebPlayer,
IWebPlayerStore
} from 'Player'
export interface IPlayerContext {
player: IWebPlayer
store: IWebPlayerStore,
}
export const defaultContextValue: IPlayerContext = { player: undefined, store: undefined}
export const PlayerContext = createContext<IPlayerContext>(defaultContextValue);

View file

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

View file

@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import cn from 'classnames';
import { closeBottomBlock } from 'Duck/components/player';
import { Input, CloseButton } from 'UI';
import { CloseButton } from 'UI';
import stl from './header.module.css';
const Header = ({

View file

@ -1,9 +0,0 @@
// import { NONE, CONSOLE, NETWORK, STACKEVENTS, REDUX_STATE, PROFILER, PERFORMANCE, GRAPHQL } from 'Duck/components/player';
//
//
// export default {
// [NONE]: {
// Component: null,
//
// }
// }

View file

@ -1,18 +0,0 @@
import React from 'react'
import { connectPlayer, jump } from 'Player';
import ConsoleContent from './ConsoleContent';
@connectPlayer(state => ({
logs: state.logList,
// time: state.time,
livePlay: state.livePlay,
listNow: state.logListNow,
}))
export default class Console extends React.PureComponent {
render() {
const { logs, time, listNow } = this.props;
return (
<ConsoleContent jump={!this.props.livePlay && jump} logs={logs} lastIndex={listNow.length - 1} logsNow={listNow} />
);
}
}

View file

@ -1,123 +0,0 @@
import React from 'react';
import cn from 'classnames';
import { getRE } from 'App/utils';
import { Icon, NoContent, Tabs, Input } from 'UI';
import { jump } from 'Player';
import { LEVEL } from 'Types/session/log';
import Autoscroll from '../Autoscroll';
import BottomBlock from '../BottomBlock';
import stl from './console.module.css';
import ConsoleRow from './ConsoleRow';
// import { Duration } from 'luxon';
const ALL = 'ALL';
const INFO = 'INFO';
const WARNINGS = 'WARNINGS';
const ERRORS = 'ERRORS';
const LEVEL_TAB = {
[LEVEL.INFO]: INFO,
[LEVEL.LOG]: INFO,
[LEVEL.WARNING]: WARNINGS,
[LEVEL.ERROR]: ERRORS,
[LEVEL.EXCEPTION]: ERRORS,
};
const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({ text: tab, key: tab }));
// eslint-disable-next-line complexity
const getIconProps = (level) => {
switch (level) {
case LEVEL.INFO:
case LEVEL.LOG:
return {
name: 'console/info',
color: 'blue2',
};
case LEVEL.WARN:
case LEVEL.WARNING:
return {
name: 'console/warning',
color: 'red2',
};
case LEVEL.ERROR:
return {
name: 'console/error',
color: 'red',
};
}
return null;
};
function renderWithNL(s = '') {
if (typeof s !== 'string') return '';
return s.split('\n').map((line, i) => <div className={cn({ 'ml-20': i !== 0 })}>{line}</div>);
}
export default class ConsoleContent extends React.PureComponent {
state = {
filter: '',
activeTab: ALL,
};
onTabClick = (activeTab) => this.setState({ activeTab });
onFilterChange = ({ target: { value } }) => this.setState({ filter: value });
render() {
const { logs, isResult, additionalHeight, logsNow } = this.props;
const time = logsNow.length > 0 ? logsNow[logsNow.length - 1].time : undefined;
const { filter, activeTab, currentError } = this.state;
const filterRE = getRE(filter, 'i');
const filtered = logs.filter(({ level, value }) =>
activeTab === ALL
? filterRE.test(value)
: filterRE.test(value) && LEVEL_TAB[level] === activeTab
);
const lastIndex = filtered.filter((item) => item.time <= time).length - 1;
return (
<>
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }}>
<BottomBlock.Header showClose={!isResult}>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Console</span>
<Tabs tabs={TABS} active={activeTab} onClick={this.onTabClick} border={false} />
</div>
<Input
className="input-small h-8"
placeholder="Filter by keyword"
icon="search"
iconPosition="left"
name="filter"
height={28}
onChange={this.onFilterChange}
/>
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent
title={
<div className="capitalize flex items-center mt-16">
<Icon name="info-circle" className="mr-2" size="18" />
No Data
</div>
}
size="small"
show={filtered.length === 0}
>
<Autoscroll autoScrollTo={Math.max(lastIndex, 0)}>
{filtered.map((l) => (
<ConsoleRow
log={l}
jump={jump}
iconProps={getIconProps(l.level)}
renderWithNL={renderWithNL}
/>
))}
</Autoscroll>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</>
);
}
}

View file

@ -41,7 +41,7 @@ function ConsoleRow(props: Props) {
)}
<span>{renderWithNL(lines.pop())}</span>
</div>
{canExpand && expanded && lines.map((l: any, i: number) => <div key={l.slice(0,3)+i} className="ml-4 mb-1">{l}</div>)}
{/* {canExpand && expanded && lines.map((l: any, i: number) => <div key={l.slice(0,3)+i} className="ml-4 mb-1">{l}</div>)} */}
</div>
<JumpButton onClick={() => jump(log.time)} />
</div>

View file

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

View file

@ -1,38 +0,0 @@
.message {
overflow-x: auto;
margin-left: 10px;
font-size: 13px;
overflow-x: auto;
&::-webkit-scrollbar {
height: 2px;
}
}
.line {
font-family: 'Menlo', 'monaco', 'consolas', monospace;
/* margin-top: -1px; ??? */
display: flex;
align-items: flex-start;
border-bottom: solid thin $gray-light-shade;
&:hover {
background-coor: $active-blue !important;
}
}
.timestamp {
}
.activeRow {
background-color: $teal-light !important;
}
.icon {
padding-top: 4px;
margin-right: 7px;
}
.inactiveRow {
opacity: 0.5;
}

View file

@ -1,9 +1,13 @@
import React, { useEffect } from 'react'
import { Input, Icon } from 'UI'
import { connectPlayer, toggleEvents, scale } from 'Player';
import { PlayerContext } from 'App/components/Session/playerContext';
function EventSearch(props) {
const { onChange, clearSearch, value, header, toggleEvents, setActiveTab } = props;
const { player } = React.useContext(PlayerContext)
const { onChange, clearSearch, value, header, setActiveTab } = props;
const toggleEvents = () => player.toggleEvents()
useEffect(() => {
return () => {
@ -42,4 +46,4 @@ function EventSearch(props) {
)
}
export default connectPlayer(() => ({}), { toggleEvents })(EventSearch)
export default EventSearch

View file

@ -141,6 +141,7 @@ export default class EventsBlock extends React.Component {
eventsIndex,
filterOutNote,
} = this.props;
const { query } = this.state;
const _events = this.eventsList
const isLastEvent = index === _events.size - 1;

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';
@ -9,7 +9,6 @@ import copy from 'copy-to-clipboard';
import { toast } from 'react-toastify';
import { session } from 'App/routes';
import { confirm } from 'UI';
import { filterOutNote as filterOutTimelineNote } from 'Player';
import { TeamBadge } from 'Shared/SessionListContainer/components/Notes';
interface Props {
@ -24,7 +23,6 @@ function NoteEvent(props: Props) {
const { settingsStore, notesStore } = useStore();
const { timezone } = settingsStore.sessionSettings;
console.log(props.noEdit);
const onEdit = () => {
props.onEdit({
isVisible: true,
@ -60,7 +58,6 @@ function NoteEvent(props: Props) {
) {
notesStore.deleteNote(props.note.noteId).then((r) => {
props.filterOutNote(props.note.noteId);
filterOutTimelineNote(props.note.noteId);
toast.success('Note deleted');
});
}

View file

@ -1,159 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { getRE } from 'App/utils';
import {
NoContent,
Loader,
Input,
ErrorItem,
SlideModal,
ErrorDetails,
ErrorHeader,
Link,
QuestionMarkHint,
Tabs,
} from 'UI';
import { fetchErrorStackList } from 'Duck/sessions';
import { connectPlayer, jump } from 'Player';
import { error as errorRoute } from 'App/routes';
import Autoscroll from '../Autoscroll';
import BottomBlock from '../BottomBlock';
@connectPlayer((state) => ({
logs: state.logListNow,
exceptions: state.exceptionsList,
// exceptionsNow: state.exceptionsListNow,
}))
@connect(
(state) => ({
session: state.getIn(['sessions', 'current']),
errorStack: state.getIn(['sessions', 'errorStack']),
sourcemapUploaded: state.getIn(['sessions', 'sourcemapUploaded']),
loading: state.getIn(['sessions', 'fetchErrorStackList', 'loading']),
}),
{ fetchErrorStackList }
)
export default class Exceptions extends React.PureComponent {
state = {
filter: '',
currentError: null,
};
onFilterChange = ({ target: { value } }) => this.setState({ filter: value });
setCurrentError = (err) => {
const { session } = this.props;
this.props.fetchErrorStackList(session.sessionId, err.errorId);
this.setState({ currentError: err });
};
closeModal = () => this.setState({ currentError: null });
render() {
const { exceptions, loading, errorStack, sourcemapUploaded } = this.props;
const { filter, currentError } = this.state;
const filterRE = getRE(filter, 'i');
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
title={
currentError && (
<div className="mb-4">
<div className="text-xl mb-2">
<Link to={errorRoute(currentError.errorId)}>
<span className="font-bold">{currentError.name}</span>
</Link>
<span className="ml-2 text-sm color-gray-medium">{currentError.function}</span>
</div>
<div>{currentError.message}</div>
</div>
)
}
isDisplayed={currentError != null}
content={
currentError && (
<div className="px-4">
<Loader loading={loading}>
<NoContent show={!loading && errorStack.size === 0} title="Nothing found!">
<ErrorDetails
error={currentError}
errorStack={errorStack}
sourcemapUploaded={sourcemapUploaded}
/>
</NoContent>
</Loader>
</div>
)
}
onClose={this.closeModal}
/>
<BottomBlock>
<BottomBlock.Header>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Exceptions</span>
</div>
<div className={'flex items-center justify-between'}>
<Input
className="input-small"
placeholder="Filter by name or message"
icon="search"
iconPosition="left"
name="filter"
onChange={this.onFilterChange}
height={28}
/>
<QuestionMarkHint
className={'mx-4'}
content={
<>
<a
className="color-teal underline"
target="_blank"
href="https://docs.openreplay.com/installation/upload-sourcemaps"
>
Upload Source Maps{' '}
</a>
and see source code context obtained from stack traces in their original form.
</>
}
/>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent size="small" show={filtered.length === 0} title="No recordings found">
<Autoscroll>
{filtered.map((e, index) => (
<ErrorItem
onJump={() => jump(e.time)}
error={e}
key={e.key}
// selected={lastIndex === index}
// inactive={index > lastIndex}
onErrorClick={(jsEvent) => {
jsEvent.stopPropagation();
jsEvent.preventDefault();
this.setCurrentError(e);
}}
/>
))}
</Autoscroll>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</>
);
}
}

View file

@ -0,0 +1,125 @@
import React from 'react';
import { connect } from 'react-redux';
import { getRE } from 'App/utils';
import {
NoContent,
Loader,
Input,
ErrorItem,
SlideModal,
ErrorDetails,
Link,
QuestionMarkHint,
} from 'UI';
import { error as errorRoute } from 'App/routes';
import Autoscroll from '../Autoscroll';
import BottomBlock from '../BottomBlock';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
interface IProps {
loading: boolean;
sourcemapUploaded: boolean;
errorStack: Record<string, any>;
}
function Exceptions({ errorStack, sourcemapUploaded, loading }: IProps) {
const { player, store } = React.useContext(PlayerContext);
const { logListNow: logs, exceptionsList: exceptions } = store.get();
const [filter, setFilter] = React.useState('');
const [currentError, setCurrentErrorVal] = React.useState(null);
const onFilterChange = ({ target: { value } }: any) => setFilter(value);
const closeModal = () => setCurrentErrorVal(null);
const filterRE = getRE(filter, 'i');
const filtered = exceptions.filter((e: any) => filterRE.test(e.name) || filterRE.test(e.message));
return (
<>
<SlideModal
title={
currentError && (
<div className="mb-4">
<div className="text-xl mb-2">
<Link to={errorRoute(currentError.errorId)}>
<span className="font-bold">{currentError.name}</span>
</Link>
<span className="ml-2 text-sm color-gray-medium">{currentError.function}</span>
</div>
<div>{currentError.message}</div>
</div>
)
}
isDisplayed={currentError != null}
content={
currentError && (
<div className="px-4">
<Loader loading={loading}>
<NoContent show={!loading && errorStack.size === 0} title="Nothing found!">
<ErrorDetails
error={currentError}
// @ts-ignore
errorStack={errorStack}
sourcemapUploaded={sourcemapUploaded}
/>
</NoContent>
</Loader>
</div>
)
}
onClose={closeModal}
/>
<BottomBlock>
<BottomBlock.Header>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Exceptions</span>
</div>
<div className={'flex items-center justify-between'}>
<Input
className="input-small"
placeholder="Filter by name or message"
icon="search"
name="filter"
onChange={onFilterChange}
height={28}
/>
<QuestionMarkHint
className={'mx-4'}
content={
<>
<a
className="color-teal underline"
target="_blank"
href="https://docs.openreplay.com/installation/upload-sourcemaps"
>
Upload Source Maps{' '}
</a>
and see source code context obtained from stack traces in their original form.
</>
}
/>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent size="small" show={filtered.length === 0} title="No recordings found">
<Autoscroll>
{filtered.map((e: any, index) => (
<React.Fragment key={e.key}>
<ErrorItem onJump={() => player.jump(e.time)} error={e} />
</React.Fragment>
))}
</Autoscroll>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</>
);
}
export default connect((state: any) => ({
errorStack: state.getIn(['sessions', 'errorStack']),
sourcemapUploaded: state.getIn(['sessions', 'sourcemapUploaded']),
loading: state.getIn(['sessions', 'fetchErrorStackList', 'loading']),
}))(observer(Exceptions));

View file

@ -6,8 +6,6 @@ export default class GQLDetails extends React.PureComponent {
render() {
const {
gql: { variables, response, duration, operationKind, operationName },
nextClick,
prevClick,
first = false,
last = false,
} = this.props;
@ -57,15 +55,6 @@ export default class GQLDetails extends React.PureComponent {
</div>
</div>
</div>
<div className="flex justify-between absolute bottom-0 left-0 right-0 p-3 border-t bg-white">
<Button variant="outline" onClick={prevClick} disabled={first}>
Prev
</Button>
<Button variant="outline" onClick={nextClick} disabled={last}>
Next
</Button>
</div>
</div>
);
}

View file

@ -1,178 +0,0 @@
import React from 'react';
import { NoContent, Input, SlideModal, CloseButton, Button } from 'UI';
import { getRE } from 'App/utils';
import { connectPlayer, pause, jump } from 'Player';
import BottomBlock from '../BottomBlock';
import TimeTable from '../TimeTable';
import GQLDetails from './GQLDetails';
import { renderStart } from 'Components/Session_/Network/NetworkContent';
function renderDefaultStatus() {
return '2xx-3xx';
}
export function renderName(r) {
return (
<div className="flex justify-between items-center grow-0 w-full">
<div>{r.operationName}</div>
<Button
variant="text"
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
onClick={(e) => {
e.stopPropagation();
jump(r.time);
}}
>
Jump
</Button>
</div>
);
}
@connectPlayer((state) => ({
list: state.graphqlList,
listNow: state.graphqlListNow,
time: state.time,
livePlay: state.livePlay,
}))
export default class GraphQL extends React.PureComponent {
state = {
filter: '',
filteredList: this.props.list,
filteredListNow: this.props.listNow,
current: null,
currentIndex: 0,
showFetchDetails: false,
hasNextError: false,
hasPreviousError: false,
lastActiveItem: 0,
};
static filterList(list, value) {
const filterRE = getRE(value, 'i');
return value
? list.filter(
(r) =>
filterRE.test(r.operationKind) ||
filterRE.test(r.operationName) ||
filterRE.test(r.variables)
)
: list;
}
onFilterChange = ({ target: { value } }) => {
const { list } = this.props;
const filtered = GraphQL.filterList(list, value);
this.setState({ filter: value, filteredList: filtered, currentIndex: 0 });
};
setCurrent = (item, index) => {
if (!this.props.livePlay) {
pause();
jump(item.time);
}
this.setState({ current: item, currentIndex: index });
};
closeModal = () => this.setState({ current: null, showFetchDetails: false });
static getDerivedStateFromProps(nextProps, prevState) {
const { list } = nextProps;
if (nextProps.time) {
const filtered = GraphQL.filterList(list, prevState.filter);
let i = 0;
filtered.forEach((item, index) => {
if (item.time <= nextProps.time) {
i = index;
}
});
return {
lastActiveItem: i,
};
}
}
render() {
const { current, currentIndex, filteredList, lastActiveItem } = this.state;
return (
<React.Fragment>
<SlideModal
size="middle"
right
title={
<div className="flex justify-between">
<h1>GraphQL</h1>
<div className="flex items-center">
<CloseButton onClick={this.closeModal} size="18" className="ml-2" />
</div>
</div>
}
isDisplayed={current != null}
content={
current && (
<GQLDetails
gql={current}
nextClick={this.nextClickHander}
prevClick={this.prevClickHander}
first={currentIndex === 0}
last={currentIndex === filteredList.length - 1}
/>
)
}
onClose={this.closeModal}
/>
<BottomBlock>
<BottomBlock.Header>
<span className="font-semibold color-gray-medium mr-4">GraphQL</span>
<div className="flex items-center">
<Input
// className="input-small"
placeholder="Filter by name or type"
icon="search"
iconPosition="left"
name="filter"
onChange={this.onFilterChange}
/>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent size="small" title="No recordings found" show={filteredList.length === 0}>
<TimeTable
rows={filteredList}
onRowClick={this.setCurrent}
hoverable
activeIndex={lastActiveItem}
>
{[
{
label: 'Start',
width: 90,
render: renderStart,
},
{
label: 'Status',
width: 70,
render: renderDefaultStatus,
},
{
label: 'Type',
dataKey: 'operationKind',
width: 60,
},
{
label: 'Name',
width: 240,
render: renderName,
},
]}
</TimeTable>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</React.Fragment>
);
}
}

View file

@ -0,0 +1,174 @@
import React, { useEffect } from 'react';
import { NoContent, Input, SlideModal, CloseButton, Button } from 'UI';
import { getRE } from 'App/utils';
import BottomBlock from '../BottomBlock';
import TimeTable from '../TimeTable';
import GQLDetails from './GQLDetails';
import { renderStart } from 'Components/Session_/Network/NetworkContent';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
function renderDefaultStatus() {
return '2xx-3xx';
}
export function renderName(r: Record<string, any>) {
const { player } = React.useContext(PlayerContext);
return (
<div className="flex justify-between items-center grow-0 w-full">
<div>{r.operationName}</div>
<Button
variant="text"
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
player.jump(r.time);
}}
>
Jump
</Button>
</div>
);
}
function GraphQL() {
const { player, store } = React.useContext(PlayerContext);
const { graphqlList: list, graphqlListNow: listNow, time, livePlay } = store.get();
const defaultState = {
filter: '',
filteredList: list,
filteredListNow: listNow,
// @ts-ignore
current: null,
currentIndex: 0,
showFetchDetails: false,
hasNextError: false,
hasPreviousError: false,
lastActiveItem: 0,
};
const [state, setState] = React.useState(defaultState);
const filterList = (list: any, value: string) => {
const filterRE = getRE(value, 'i');
return value
? list.filter(
(r: any) =>
filterRE.test(r.operationKind) ||
filterRE.test(r.operationName) ||
filterRE.test(r.variables)
)
: list;
};
const onFilterChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
const filtered = filterList(list, value);
setState((prevState) => ({
...prevState,
filter: value,
filteredList: filtered,
currentIndex: 0,
}));
};
const setCurrent = (item: any, index: number) => {
if (!livePlay) {
player.pause();
player.jump(item.time);
}
setState((prevState) => ({ ...prevState, current: item, currentIndex: index }));
};
const closeModal = () =>
setState((prevState) => ({ ...prevState, current: null, showFetchDetails: false }));
useEffect(() => {
const filtered = filterList(listNow, state.filter);
if (filtered.length !== lastActiveItem) {
setState((prevState) => ({ ...prevState, lastActiveItem: listNow.length }));
}
}, [time]);
const { current, currentIndex, filteredList, lastActiveItem } = state;
return (
<React.Fragment>
<SlideModal
size="middle"
right
title={
<div className="flex justify-between">
<h1>GraphQL</h1>
<div className="flex items-center">
<CloseButton onClick={closeModal} size="18" className="ml-2" />
</div>
</div>
}
isDisplayed={current != null}
content={
current && (
<GQLDetails
gql={current}
first={currentIndex === 0}
last={currentIndex === filteredList.length - 1}
/>
)
}
onClose={closeModal}
/>
<BottomBlock>
<BottomBlock.Header>
<span className="font-semibold color-gray-medium mr-4">GraphQL</span>
<div className="flex items-center">
<Input
// className="input-small"
placeholder="Filter by name or type"
icon="search"
name="filter"
onChange={onFilterChange}
/>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent size="small" title="No recordings found" show={filteredList.length === 0}>
<TimeTable
rows={filteredList}
onRowClick={setCurrent}
hoverable
activeIndex={lastActiveItem}
>
{[
{
label: 'Start',
width: 90,
render: renderStart,
},
{
label: 'Status',
width: 70,
render: renderDefaultStatus,
},
{
label: 'Type',
dataKey: 'operationKind',
width: 60,
},
{
label: 'Name',
width: 240,
render: renderName,
},
]}
</TimeTable>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</React.Fragment>
);
}
export default observer(GraphQL);

View file

@ -1,27 +1,27 @@
import React, { useEffect, useState, useRef } from 'react';
import { toggleInspectorMode, markElement } from 'Player';
import ElementView from './ElementView';
import BottomBlock from '../BottomBlock';
import stl from './inspector.module.css'
import stl from './inspector.module.css';
import { PlayerContext } from 'App/components/Session/playerContext';
// TODO: refactor: use Layout from the Sessions and put everything there under the WebPlayer folder
// function onMount(element, setOpen) { // TODO: through the MobX
// element.setOpen = setOpen;
// }
export default function Inspector() {
const { player } = React.useContext(PlayerContext);
const toggleInspectorMode = player.toggleInspectorMode;
const markElement = player.mark;
export default function Inspector () {
const [doc, setDoc] = useState(null);
const [openChain, setOpenChain] = useState([]);
const [selectedElement, _setSelectedElement] = useState(null);
const selectedElementRef = useRef(selectedElement);
const setSelectedElement = elem => {
const setSelectedElement = (elem) => {
selectedElementRef.current = elem;
_setSelectedElement(elem);
}
};
useEffect(() => {
useEffect(() => {
const doc = toggleInspectorMode(true, ({ target }) => {
const openChain = [];
let currentTarget = target;
@ -33,9 +33,9 @@ export default function Inspector () {
setSelectedElement(target);
});
setDoc(doc);
setOpenChain([ doc.documentElement ]);
setOpenChain([doc.documentElement]);
const onKeyPress = e => {
const onKeyPress = (e) => {
if (e.key === 'Backspace' || e.key === 'Delete') {
const elem = selectedElementRef.current;
if (elem !== null && elem.parentElement !== null) {
@ -43,30 +43,30 @@ export default function Inspector () {
setSelectedElement(null);
}
}
}
window.addEventListener("keydown", onKeyPress);
};
window.addEventListener('keydown', onKeyPress);
return () => {
toggleInspectorMode(false);
window.removeEventListener("keydown", onKeyPress);
}
window.removeEventListener('keydown', onKeyPress);
};
}, []);
if (!doc) return null;
return (
<BottomBlock>
if (!doc) return null;
return (
<BottomBlock>
<BottomBlock.Content>
<div onMouseLeave={ () => markElement(null) } className={stl.wrapper}>
<ElementView
element={ doc.documentElement }
<div onMouseLeave={() => markElement(null)} className={stl.wrapper}>
<ElementView
element={doc.documentElement}
level={0}
context={doc.defaultView}
openChain={ openChain }
selectedElement={ selectedElement }
setSelectedElement={ setSelectedElement }
onHover={ markElement }
openChain={openChain}
selectedElement={selectedElement}
setSelectedElement={setSelectedElement}
onHover={(e) => markElement(e)}
/>
</div>
</BottomBlock.Content>
</BottomBlock>
);
}
);
}

View file

@ -40,12 +40,12 @@ export default class GraphQL extends React.PureComponent {
const { filter, current } = this.state;
const filterRE = getRE(filter, 'i');
const filtered = list
.filter(({ containerType, context, containerName = "", containerId = "", containerSrc="" }) =>
filterRE.test(containerName) ||
.filter(({ containerType, context, containerName = "", containerId = "", containerSrc="" }) =>
filterRE.test(containerName) ||
filterRE.test(containerId) ||
filterRE.test(containerSrc) ||
filterRE.test(CONTEXTS[ context ]) ||
filterRE.test(CONTAINER_TYPES[ containerType ]));
filterRE.test(CONTAINER_TYPES[ containerType ]));
const lastIndex = filtered.filter(item => item.time <= time).length - 1;
return (
<BottomBlock>
@ -64,7 +64,7 @@ export default class GraphQL extends React.PureComponent {
<QuestionMarkHint
content={
<>
<a className="color-teal underline mr-2" target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Long_Tasks_API">Learn more </a>
<a className="color-teal underline mr-2" target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Long_Tasks_API">Learn more </a>
about Long Tasks API
</>
}

View file

@ -1 +1 @@
export { default } from './LongTasks';
export { default } from './LongTasks.DEPRECATED';

View file

@ -1,7 +1,7 @@
import React from 'react';
import cn from 'classnames';
import { connectPlayer, jump, pause } from 'Player';
import { Tooltip, Button, TextEllipsis } from 'UI';
import { connectPlayer, } from 'Player';
import { Tooltip, TextEllipsis } from 'UI';
import { getRE } from 'App/utils';
import { TYPES } from 'Types/session/resource';
import stl from './network.module.css';

View file

@ -1,6 +1,5 @@
import React from 'react';
import cn from 'classnames';
// import { connectPlayer } from 'Player';
import { QuestionMarkHint, Tooltip, Tabs, Input, NoContent, Icon, Toggler, Button } from 'UI';
import { getRE } from 'App/utils';
import { TYPES } from 'Types/session/resource';
@ -12,7 +11,6 @@ import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
import stl from './network.module.css';
import { Duration } from 'luxon';
import { jump } from 'Player';
const ALL = 'ALL';
const XHR = 'xhr';
@ -112,8 +110,6 @@ function renderSize(r) {
content = 'Not captured';
} else {
const headerSize = r.headerSize || 0;
const encodedSize = r.encodedBodySize || 0;
const transferred = headerSize + encodedSize;
const showTransferred = r.headerSize != null;
triggerText = formatBytes(r.decodedBodySize);
@ -234,7 +230,6 @@ export default class NetworkContent extends React.PureComponent {
className="input-small"
placeholder="Filter by name"
icon="search"
iconPosition="left"
name="filter"
onChange={this.onFilterChange}
height={28}

View file

@ -1,6 +1,5 @@
import { connectPlayer } from 'Player';
import { toggleBottomBlock } from 'Duck/components/player';
import React, { useEffect } from 'react';
import { toggleBottomBlock } from 'Duck/components/player';
import BottomBlock from '../BottomBlock';
import EventRow from './components/EventRow';
import { TYPES } from 'Types/session/event';
@ -10,40 +9,39 @@ import FeatureSelection, { HELP_MESSAGE } from './components/FeatureSelection/Fe
import TimelinePointer from './components/TimelinePointer';
import VerticalPointerLine from './components/VerticalPointerLine';
import cn from 'classnames';
// import VerticalLine from './components/VerticalLine';
import OverviewPanelContainer from './components/OverviewPanelContainer';
import { NoContent, Icon } from 'UI';
import { observer } from 'mobx-react-lite';
import { PlayerContext } from 'App/components/Session/playerContext';
interface Props {
resourceList: any[];
exceptionsList: any[];
eventsList: any[];
toggleBottomBlock: any;
stackEventList: any[];
issuesList: any[];
performanceChartData: any;
endTime: number;
fetchPresented?: boolean;
}
function OverviewPanel(props: Props) {
const { fetchPresented = false } = props;
function OverviewPanel({ issuesList }: { issuesList: Record<string, any>[] }) {
const { store } = React.useContext(PlayerContext)
const [dataLoaded, setDataLoaded] = React.useState(false);
const [selectedFeatures, setSelectedFeatures] = React.useState([
'PERFORMANCE',
'ERRORS',
// 'EVENTS',
'NETWORK',
]);
const resources: any = React.useMemo(() => {
const {
resourceList,
exceptionsList,
eventsList,
stackEventList,
issuesList,
endTime,
performanceChartData,
} = props;
stackList: stackEventList,
eventList: eventsList,
exceptionsList,
resourceList: resourceListUnmap,
fetchList,
graphqlList,
} = store.get()
const fetchPresented = fetchList.length > 0;
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))
const resources: any = React.useMemo(() => {
return {
NETWORK: resourceList,
ERRORS: exceptionsList,
@ -59,25 +57,25 @@ function OverviewPanel(props: Props) {
}
if (
props.resourceList.length > 0 ||
props.exceptionsList.length > 0 ||
props.eventsList.length > 0 ||
props.stackEventList.length > 0 ||
props.issuesList.length > 0 ||
props.performanceChartData.length > 0
resourceList.length > 0 ||
exceptionsList.length > 0 ||
eventsList.length > 0 ||
stackEventList.length > 0 ||
issuesList.length > 0 ||
performanceChartData.length > 0
) {
setDataLoaded(true);
}
}, [
props.resourceList,
props.exceptionsList,
props.eventsList,
props.stackEventList,
props.performanceChartData,
resourceList,
issuesList,
exceptionsList,
eventsList,
stackEventList,
performanceChartData,
]);
return (
<Wrapper {...props}>
<BottomBlock style={{ height: '245px' }}>
<BottomBlock.Header>
<span className="font-semibold color-gray-medium mr-4">X-RAY</span>
@ -86,9 +84,10 @@ function OverviewPanel(props: Props) {
</div>
</BottomBlock.Header>
<BottomBlock.Content>
<OverviewPanelContainer endTime={props.endTime}>
<TimelineScale endTime={props.endTime} />
<OverviewPanelContainer endTime={endTime}>
<TimelineScale endTime={endTime} />
<div
// style={{ width: '100%', height: '187px', overflow: 'hidden' }}
style={{ width: 'calc(100vw - 1rem)', margin: '0 auto', height: '187px' }}
className="transition relative"
>
@ -118,7 +117,7 @@ function OverviewPanel(props: Props) {
fetchPresented={fetchPresented}
/>
)}
endTime={props.endTime}
endTime={endTime}
message={HELP_MESSAGE[feature]}
/>
</div>
@ -128,7 +127,6 @@ function OverviewPanel(props: Props) {
</OverviewPanelContainer>
</BottomBlock.Content>
</BottomBlock>
</Wrapper>
);
}
@ -140,20 +138,5 @@ export default connect(
toggleBottomBlock,
}
)(
connectPlayer((state: any) => ({
fetchPresented: state.fetchList.length > 0,
resourceList: state.resourceList
.filter((r: any) => r.isRed() || r.isYellow())
.concat(state.fetchList.filter((i: any) => parseInt(i.status) >= 400))
.concat(state.graphqlList.filter((i: any) => parseInt(i.status) >= 400)),
exceptionsList: state.exceptionsList,
eventsList: state.eventList,
stackEventList: state.stackList,
performanceChartData: state.performanceChartData,
endTime: state.endTime,
}))(OverviewPanel)
);
const Wrapper = React.memo((props: any) => {
return <>{props.children}</>;
});
observer(OverviewPanel)
)

View file

@ -1,48 +1,33 @@
import React from 'react';
import VerticalLine from '../VerticalLine';
import { connectPlayer, Controls } from 'Player';
import { PlayerContext } from 'App/components/Session/playerContext';
interface Props {
children: React.ReactNode;
endTime: number;
children: React.ReactNode;
endTime: number;
}
const OverviewPanelContainer = React.memo((props: Props) => {
const { endTime } = props;
const [mouseX, setMouseX] = React.useState(0);
const [mouseIn, setMouseIn] = React.useState(false);
const onClickTrack = (e: any) => {
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
if (time) {
Controls.jump(time);
}
};
const { player } = React.useContext(PlayerContext)
// const onMouseMoveCapture = (e: any) => {
// if (!mouseIn) {
// return;
// }
// const p = e.nativeEvent.offsetX / e.target.offsetWidth;
// setMouseX(p * 100);
// };
const { endTime } = props;
const [mouseX, setMouseX] = React.useState(0);
const [mouseIn, setMouseIn] = React.useState(false);
const onClickTrack = (e: any) => {
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
if (time) {
player.jump(time);
}
};
return (
<div
className="overflow-x-auto overflow-y-hidden bg-gray-lightest"
onClick={onClickTrack}
// onMouseMoveCapture={onMouseMoveCapture}
// onMouseOver={() => setMouseIn(true)}
// onMouseOut={() => setMouseIn(false)}
>
{mouseIn && <VerticalLine left={mouseX} className="border-gray-medium" />}
<div className="">{props.children}</div>
</div>
);
return (
<div className="overflow-x-auto overflow-y-hidden bg-gray-lightest" onClick={onClickTrack}>
{mouseIn && <VerticalLine left={mouseX} className="border-gray-medium" />}
<div className="">{props.children}</div>
</div>
);
});
export default OverviewPanelContainer;
// export default connectPlayer((state: any) => ({
// endTime: state.endTime,
// }))(OverviewPanelContainer);

View file

@ -1,6 +1,5 @@
import React from 'react';
import { connectPlayer } from 'Player';
import { AreaChart, Area, Tooltip, ResponsiveContainer } from 'recharts';
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
interface Props {
list: any;

View file

@ -1,5 +1,4 @@
import React from 'react';
import { Controls } from 'Player';
import { NETWORK, EXCEPTIONS } from 'Duck/components/player';
import { useModal } from 'App/components/Modal';
import { Icon, Tooltip } from 'UI';
@ -7,6 +6,7 @@ import StackEventModal from '../StackEventModal';
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
import FetchDetails from 'Shared/FetchDetailsModal';
import GraphQLDetailsModal from 'Shared/GraphQLDetailsModal';
import { PlayerContext } from 'App/components/Session/playerContext';
interface Props {
pointer: any;
@ -15,11 +15,13 @@ interface Props {
fetchPresented?: boolean;
}
const TimelinePointer = React.memo((props: Props) => {
const { player } = React.useContext(PlayerContext)
const { showModal } = useModal();
const createEventClickHandler = (pointer: any, type: any) => (e: any) => {
if (props.noClick) return;
e.stopPropagation();
Controls.jump(pointer.time);
player.jump(pointer.time);
if (!type) {
return;
}

View file

@ -1,5 +1,4 @@
import React from 'react';
import { connectPlayer } from 'Player';
import { millisToMinutesAndSeconds } from 'App/utils';
interface Props {
@ -17,9 +16,6 @@ function TimelineScale(props: Props) {
for (var i = 0; i < part; i++) {
const txt = millisToMinutesAndSeconds(i * (endTime / part));
const el = document.createElement('div');
// el.style.height = '10px';
// el.style.width = '1px';
// el.style.backgroundColor = '#ccc';
el.style.position = 'absolute';
el.style.left = `${i * gap}px`;
el.style.paddingTop = '1px';
@ -38,23 +34,11 @@ function TimelineScale(props: Props) {
}
drawScale(scaleRef.current);
// const resize = () => drawScale(scaleRef.current);
// window.addEventListener('resize', resize);
// return () => {
// window.removeEventListener('resize', resize);
// };
}, [scaleRef]);
return (
<div className="h-6 bg-gray-darkest w-full" ref={scaleRef}>
{/* <div ref={scaleRef} className="w-full h-10 bg-gray-300 relative"></div> */}
</div>
);
}
export default TimelineScale;
// export default connectPlayer((state: any) => ({
// endTime: state.endTime,
// }))(TimelineScale);

View file

@ -1,18 +1,16 @@
import React from 'react';
import { connectPlayer } from 'Player';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import VerticalLine from '../VerticalLine';
interface Props {
time?: number;
scale?: number;
}
function VerticalPointerLine(props: Props) {
const { time, scale } = props;
function VerticalPointerLine() {
const { store } = React.useContext(PlayerContext)
const { time, endTime } = store.get();
const scale = 100 / endTime;
const left = time * scale;
return <VerticalLine left={left} className="border-teal" />;
}
export default connectPlayer((state: any) => ({
time: state.time,
scale: 100 / state.endTime,
}))(VerticalPointerLine);
export default observer(VerticalPointerLine);

View file

@ -3,7 +3,8 @@ import { Loader, Icon } from 'UI';
import { connect } from 'react-redux';
import { fetchInsights } from 'Duck/sessions';
import SelectorsList from './components/SelectorsList/SelectorsList';
import { markTargets, Controls as Player } from 'Player';
import { PlayerContext } from 'App/components/Session/playerContext';
import Select from 'Shared/Select';
import SelectDateRange from 'Shared/SelectDateRange';
import Period from 'Types/app/period';
@ -22,6 +23,9 @@ interface Props {
}
function PageInsightsPanel({ filters, fetchInsights, events = [], insights, urlOptions, host, loading = true, setActiveTab }: Props) {
const { player: Player } = React.useContext(PlayerContext)
const markTargets = (t: any) => Player.markTargets(t)
const [insightsFilters, setInsightsFilters] = useState(filters);
const defaultValue = urlOptions && urlOptions[0] ? urlOptions[0].value : '';

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

@ -1,16 +1,15 @@
import React from 'react';
import { NoContent } from 'UI';
import { connectPlayer } from 'Player';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import SelectorCard from '../SelectorCard/SelectorCard';
import type { MarkedTarget } from 'Player';
import stl from './selectorList.module.css';
interface Props {
targets: Array<MarkedTarget>;
activeTargetIndex: number;
}
function SelectorsList() {
const { store } = React.useContext(PlayerContext)
const { markedTargets: targets, activeTargetIndex } = store.get()
function SelectorsList({ targets, activeTargetIndex }: Props) {
return (
<NoContent title="No data available." size="small" show={targets && targets.length === 0}>
<div className={stl.wrapper}>
@ -20,7 +19,4 @@ function SelectorsList({ targets, activeTargetIndex }: Props) {
);
}
export default connectPlayer((state: any) => ({
targets: state.markedTargets,
activeTargetIndex: state.activeTargetIndex,
}))(SelectorsList);
export default observer(SelectorsList);

View file

@ -1,6 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { Controls as PlayerControls, connectPlayer } from 'Player';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import {
AreaChart,
Area,
@ -11,10 +12,8 @@ import {
Tooltip,
ResponsiveContainer,
ReferenceLine,
CartesianGrid,
Label,
} from 'recharts';
import { Checkbox } from 'UI';
import { durationFromMsFormatted } from 'App/date';
import { formatBytes } from 'App/utils';
@ -174,362 +173,324 @@ function addFpsMetadata(data) {
});
}
@connect((state) => ({
userDeviceHeapSize: state.getIn(['sessions', 'current', 'userDeviceHeapSize']),
userDeviceMemorySize: state.getIn(['sessions', 'current', 'userDeviceMemorySize']),
}))
export default class Performance extends React.PureComponent {
_timeTicks = generateTicks(this.props.performanceChartData);
_data = addFpsMetadata(this.props.performanceChartData);
// state = {
// totalHeap: false,
// usedHeap: true,
// fps: true,
// }
// onCheckboxClick = (e, { name, checked }) => this.setState({ [ name ]: checked })
function Performance({
userDeviceHeapSize,
}: {
userDeviceHeapSize: number;
}) {
const { player, store } = React.useContext(PlayerContext);
const [_timeTicks, setTicks] = React.useState<number[]>([])
const [_data, setData] = React.useState<any[]>([])
onDotClick = ({ index }) => {
const point = this._data[index];
const {
performanceChartTime,
performanceChartData,
connType,
connBandwidth,
performanceAvaliability: avaliability,
} = store.get();
React.useState(() => {
setTicks(generateTicks(performanceChartData));
setData(addFpsMetadata(performanceChartData));
})
const onDotClick = ({ index: pointer }: { index: number }) => {
const point = _data[pointer];
if (!!point) {
PlayerControls.jump(point.time);
player.jump(point.time);
}
};
onChartClick = (e) => {
const onChartClick = (e: any) => {
if (e === null) return;
const { activeTooltipIndex } = e;
const point = this._data[activeTooltipIndex];
const point = _data[activeTooltipIndex];
if (!!point) {
PlayerControls.jump(point.time);
player.jump(point.time);
}
};
render() {
const {
userDeviceHeapSize,
userDeviceMemorySize,
connType,
connBandwidth,
performanceChartTime,
avaliability = {},
} = this.props;
const { fps, cpu, heap, nodes } = avaliability;
const avaliableCount = [fps, cpu, heap, nodes].reduce((c, av) => (av ? c + 1 : c), 0);
const height = avaliableCount === 0 ? '0' : `${100 / avaliableCount}%`;
const { fps, cpu, heap, nodes } = avaliability;
const avaliableCount = [fps, cpu, heap, nodes].reduce((c, av) => (av ? c + 1 : c), 0);
const height = avaliableCount === 0 ? '0' : `${100 / avaliableCount}%`;
return (
<BottomBlock>
<BottomBlock.Header>
<div className="flex items-center w-full">
<div className="font-semibold color-gray-medium mr-auto">Performance</div>
<InfoLine>
<InfoLine.Point
label="Device Heap Size"
value={formatBytes(userDeviceHeapSize)}
display={true}
/>
{/* <InfoLine.Point */}
{/* label="Device Memory Size" */}
{/* value={ formatBytes(userDeviceMemorySize*1e6) } */}
{/* /> */}
<InfoLine.Point
label="Connection Type"
value={connType}
display={connType != null && connType !== 'unknown'}
/>
<InfoLine.Point
label="Connection Speed"
value={
connBandwidth >= 1000 ? `${connBandwidth / 1000} Mbps` : `${connBandwidth} Kbps`
}
display={connBandwidth != null}
/>
</InfoLine>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
{fps && (
<ResponsiveContainer height={height}>
<AreaChart
onClick={this.onChartClick}
data={this._data}
syncId="s"
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
return (
<BottomBlock>
<BottomBlock.Header>
<div className="flex items-center w-full">
<div className="font-semibold color-gray-medium mr-auto">Performance</div>
<InfoLine>
<InfoLine.Point
label="Device Heap Size"
value={formatBytes(userDeviceHeapSize)}
display={true}
/>
<InfoLine.Point
label="Connection Type"
value={connType}
display={connType != null && connType !== 'unknown'}
/>
<InfoLine.Point
label="Connection Speed"
value={
connBandwidth >= 1000 ? `${connBandwidth / 1000} Mbps` : `${connBandwidth} Kbps`
}
display={connBandwidth != null}
/>
</InfoLine>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
{fps && (
<ResponsiveContainer height={height}>
<AreaChart
onClick={onChartClick}
data={_data}
syncId="s"
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<Gradient id="fpsGradient" color={FPS_COLOR} />
</defs>
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={durationFromMsFormatted}
tick={{ fontSize: '12px', fill: '#333' }}
domain={[0, 'dataMax']}
ticks={_timeTicks}
>
<defs>
<Gradient id="fpsGradient" color={FPS_COLOR} />
</defs>
{/* <CartesianGrid strokeDasharray="1 1" stroke="#ddd" horizontal={ false } /> */}
{/* <CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" /> */}
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={durationFromMsFormatted}
tick={{ fontSize: '12px', fill: '#333' }}
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="FPS" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis axisLine={false} tick={false} mirror domain={[0, 85]} />
{/* <YAxis */}
{/* yAxisId="r" */}
{/* axisLine={ false } */}
{/* tick={ false } */}
{/* mirror */}
{/* domain={[0, 120]} */}
{/* orientation="right" */}
{/* /> */}
<Area
dataKey="fps"
type="stepBefore"
stroke={FPS_STROKE_COLOR}
fill="url(#fpsGradient)"
dot={false}
activeDot={{
onClick: this.onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<Area
dataKey="fpsLowMarker"
type="stepBefore"
stroke="none"
fill={FPS_LOW_COLOR}
activeDot={false}
isAnimationActive={false}
/>
<Area
dataKey="fpsVeryLowMarker"
type="stepBefore"
stroke="none"
fill={FPS_VERY_LOW_COLOR}
activeDot={false}
isAnimationActive={false}
/>
<Area
dataKey="hiddenScreenMarker"
type="stepBefore"
stroke="none"
fill={HIDDEN_SCREEN_COLOR}
activeDot={false}
isAnimationActive={false}
/>
{/* <Area */}
{/* yAxisId="r" */}
{/* dataKey="cpu" */}
{/* type="monotone" */}
{/* stroke={CPU_COLOR} */}
{/* fill="none" */}
{/* // fill="url(#fpsGradient)" */}
{/* dot={false} */}
{/* activeDot={{ */}
{/* onClick: this.onDotClick, */}
{/* style: { cursor: "pointer" }, */}
{/* }} */}
{/* isAnimationActive={ false } */}
{/* /> */}
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={FPSTooltip} filterNull={false} />
</AreaChart>
</ResponsiveContainer>
)}
{cpu && (
<ResponsiveContainer height={height}>
<AreaChart
onClick={this.onChartClick}
data={this._data}
syncId="s"
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
<Label value="FPS" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis axisLine={false} tick={false} mirror domain={[0, 85]} />
<Area
dataKey="fps"
type="stepBefore"
stroke={FPS_STROKE_COLOR}
fill="url(#fpsGradient)"
dot={false}
activeDot={{
onClick: onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<Area
dataKey="fpsLowMarker"
type="stepBefore"
stroke="none"
fill={FPS_LOW_COLOR}
activeDot={false}
isAnimationActive={false}
/>
<Area
dataKey="fpsVeryLowMarker"
type="stepBefore"
stroke="none"
fill={FPS_VERY_LOW_COLOR}
activeDot={false}
isAnimationActive={false}
/>
<Area
dataKey="hiddenScreenMarker"
type="stepBefore"
stroke="none"
fill={HIDDEN_SCREEN_COLOR}
activeDot={false}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={FPSTooltip} filterNull={false} />
</AreaChart>
</ResponsiveContainer>
)}
{cpu && (
<ResponsiveContainer height={height}>
<AreaChart
onClick={onChartClick}
data={_data}
syncId="s"
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<Gradient id="cpuGradient" color={CPU_COLOR} />
</defs>
{/* <CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" /> */}
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={() => ''}
domain={[0, 'dataMax']}
ticks={_timeTicks}
>
<defs>
<Gradient id="cpuGradient" color={CPU_COLOR} />
</defs>
{/* <CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" /> */}
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={() => ''}
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="CPU" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis axisLine={false} tick={false} mirror domain={[0, 120]} orientation="right" />
<Area
dataKey="cpu"
type="monotone"
stroke={CPU_STROKE_COLOR}
// fill="none"
fill="url(#cpuGradient)"
dot={false}
activeDot={{
onClick: this.onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<Area
dataKey="hiddenScreenMarker"
type="stepBefore"
stroke="none"
fill={HIDDEN_SCREEN_COLOR}
activeDot={false}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={CPUTooltip} filterNull={false} />
</AreaChart>
</ResponsiveContainer>
)}
<Label value="CPU" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis axisLine={false} tick={false} mirror domain={[0, 120]} orientation="right" />
<Area
dataKey="cpu"
type="monotone"
stroke={CPU_STROKE_COLOR}
fill="url(#cpuGradient)"
dot={false}
activeDot={{
onClick: onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<Area
dataKey="hiddenScreenMarker"
type="stepBefore"
stroke="none"
fill={HIDDEN_SCREEN_COLOR}
activeDot={false}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={CPUTooltip} filterNull={false} />
</AreaChart>
</ResponsiveContainer>
)}
{heap && (
<ResponsiveContainer height={height}>
<ComposedChart
onClick={this.onChartClick}
data={this._data}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
syncId="s"
{heap && (
<ResponsiveContainer height={height}>
<ComposedChart
onClick={onChartClick}
data={_data}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
syncId="s"
>
<defs>
<Gradient id="usedHeapGradient" color={USED_HEAP_COLOR} />
</defs>
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={() => ''} // tick={false} + _timeTicks to cartesian array
domain={[0, 'dataMax']}
ticks={_timeTicks}
>
<defs>
<Gradient id="usedHeapGradient" color={USED_HEAP_COLOR} />
</defs>
{/* <CartesianGrid strokeDasharray="1 1" stroke="#ddd" horizontal={ false } /> */}
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={() => ''} // tick={false} + this._timeTicks to cartesian array
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="HEAP" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis
axisLine={false}
tickFormatter={formatBytes}
mirror
// Hack to keep only end tick
minTickGap={Number.MAX_SAFE_INTEGER}
domain={[0, (max) => max * 1.2]}
/>
<Line
type="monotone"
dataKey="totalHeap"
// fill="url(#totalHeapGradient)"
stroke={TOTAL_HEAP_STROKE_COLOR}
dot={false}
activeDot={{
onClick: this.onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<Area
dataKey="usedHeap"
type="monotone"
fill="url(#usedHeapGradient)"
stroke={USED_HEAP_STROKE_COLOR}
dot={false}
activeDot={{
onClick: this.onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={HeapTooltip} filterNull={false} />
</ComposedChart>
</ResponsiveContainer>
)}
{nodes && (
<ResponsiveContainer height={height}>
<AreaChart
onClick={this.onChartClick}
data={this._data}
syncId="s"
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
<Label value="HEAP" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis
axisLine={false}
tickFormatter={formatBytes}
mirror
// Hack to keep only end tick
minTickGap={Number.MAX_SAFE_INTEGER}
domain={[0, (max: number) => max * 1.2]}
/>
<Line
type="monotone"
dataKey="totalHeap"
stroke={TOTAL_HEAP_STROKE_COLOR}
dot={false}
activeDot={{
onClick: onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<Area
dataKey="usedHeap"
type="monotone"
fill="url(#usedHeapGradient)"
stroke={USED_HEAP_STROKE_COLOR}
dot={false}
activeDot={{
onClick: onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={HeapTooltip} filterNull={false} />
</ComposedChart>
</ResponsiveContainer>
)}
{nodes && (
<ResponsiveContainer height={height}>
<AreaChart
onClick={onChartClick}
data={_data}
syncId="s"
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<Gradient id="nodesGradient" color={NODES_COUNT_COLOR} />
</defs>
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={() => ''}
domain={[0, 'dataMax']}
ticks={_timeTicks}
>
<defs>
<Gradient id="nodesGradient" color={NODES_COUNT_COLOR} />
</defs>
{/* <CartesianGrid strokeDasharray="1 1" stroke="#ddd" horizontal={ false } /> */}
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={() => ''}
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="NODES" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis
axisLine={false}
tick={false}
mirror
orientation="right"
domain={[0, (max) => max * 1.2]}
/>
<Area
dataKey="nodesCount"
type="monotone"
stroke={NODES_COUNT_STROKE_COLOR}
// fill="none"
fill="url(#nodesGradient)"
dot={false}
activeDot={{
onClick: this.onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={NodesCountTooltip} filterNull={false} />
</AreaChart>
</ResponsiveContainer>
)}
</BottomBlock.Content>
</BottomBlock>
);
}
<Label value="NODES" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis
axisLine={false}
tick={false}
mirror
orientation="right"
domain={[0, (max: number) => max * 1.2]}
/>
<Area
dataKey="nodesCount"
type="monotone"
stroke={NODES_COUNT_STROKE_COLOR}
fill="url(#nodesGradient)"
dot={false}
activeDot={{
onClick: onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={NodesCountTooltip} filterNull={false} />
</AreaChart>
</ResponsiveContainer>
)}
</BottomBlock.Content>
</BottomBlock>
);
}
export const ConnectedPerformance = connectPlayer((state) => ({
performanceChartTime: state.performanceChartTime,
performanceChartData: state.performanceChartData,
connType: state.connType,
connBandwidth: state.connBandwidth,
avaliability: state.performanceAvaliability,
}))(Performance);
export const ConnectedPerformance = connect((state: any) => ({
userDeviceHeapSize: state.getIn(['sessions', 'current', 'userDeviceHeapSize']),
}))(observer(Performance));

View file

@ -1,2 +1,2 @@
export { default } from './Performance';
export * from './Performance';
export { ConnectedPerformance as default } from './Performance';
export * from './Performance';

View file

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

View file

@ -0,0 +1,420 @@
import React from 'react';
import cn from 'classnames';
import { connect } from 'react-redux';
import { STORAGE_TYPES, selectStorageType } 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 profilesCount = profilesList.length;
const graphqlCount = graphqlList.length;
const showGraphql = graphqlCount > 0;
const showProfiler = profilesCount > 0;
const showExceptions = exceptionsList.length > 0;
const showStorage = storageType !== STORAGE_TYPES.NONE || showStorageRedux;
// const fetchCount = fetchList.length;
// const stackCount = stackList.length;
// 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') {
player.speedDown();
}
if (e.key === 'ArrowUp') {
player.speedUp();
}
};
React.useEffect(() => {
document.addEventListener('keydown', onKeyDown.bind(this));
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 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) {
// player.toggleInspectorMode(false);
bottomBlock && toggleBottomBlock();
} else {
// player.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

@ -1,7 +1,8 @@
import React from 'react';
import { Duration } from 'luxon';
import { connectPlayer } from 'Player';
import styles from './time.module.css';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
const Time = ({ time, isCustom, format = 'm:ss', }) => (
<div className={ !isCustom ? styles.time : undefined }>
@ -11,19 +12,17 @@ const Time = ({ time, isCustom, format = 'm:ss', }) => (
Time.displayName = "Time";
const ReduxTime = connectPlayer((state, { name, format }) => ({
time: state[ name ],
format,
}))(Time);
const ReduxTime = observer(({ format, name, isCustom }) => {
const { store } = React.useContext(PlayerContext)
const time = store.get()[name]
return <Time format={format} time={time} isCustom={isCustom} />
})
const AssistDurationCont = () => {
const { store } = React.useContext(PlayerContext)
const { assistStart } = store.get()
const AssistDurationCont = connectPlayer(
state => {
const assistStart = state.assistStart;
return {
assistStart,
}
}
)(({ assistStart }) => {
const [assistDuration, setAssistDuration] = React.useState('00:00');
React.useEffect(() => {
const interval = setInterval(() => {
@ -37,9 +36,9 @@ const AssistDurationCont = connectPlayer(
Elapsed {assistDuration}
</>
)
})
}
const AssistDuration = React.memo(AssistDurationCont);
const AssistDuration = observer(AssistDurationCont)
ReduxTime.displayName = "ReduxTime";

View file

@ -1,19 +1,22 @@
import React from 'react';
import { connectPlayer } from 'Player';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import styles from './timeTracker.module.css';
import cn from 'classnames'
const TimeTracker = ({ time, scale, live, left }) => (
const TimeTracker = ({ scale, live, left }) => {
const { store } = React.useContext(PlayerContext)
const time = store.get().time
return (
<React.Fragment>
<span
className={ cn(styles.playedTimeline, live && left > 99 ? styles.liveTime : null) }
style={ { width: `${ time * scale }%` } }
/>
</React.Fragment>
);
);}
TimeTracker.displayName = 'TimeTracker';
export default connectPlayer(state => ({
time: state.time,
}))(TimeTracker);
export default observer(TimeTracker);

View file

@ -1,7 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { Icon } from 'UI'
import { connectPlayer, Controls, toggleTimetravel } from 'Player';
import TimeTracker from './TimeTracker';
import stl from './timeline.module.css';
import { setTimelinePointer, setTimelineHoverTime } from 'Duck/sessions';
@ -9,6 +8,9 @@ import DraggableCircle from './DraggableCircle';
import CustomDragLayer from './CustomDragLayer';
import { debounce } from 'App/utils';
import TooltipContainer from './components/TooltipContainer';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
const BOUNDRY = 0;
@ -20,105 +22,64 @@ function getTimelinePosition(value, scale) {
let deboucneJump = () => null;
let debounceTooltipChange = () => null;
@connectPlayer((state) => ({
playing: state.playing,
time: state.time,
skipIntervals: state.skipIntervals,
events: state.eventList,
skip: state.skip,
skipToIssue: state.skipToIssue,
disabled: state.cssLoading || state.messagesLoading || state.markedTargets,
endTime: state.endTime,
live: state.live,
notes: state.notes,
}))
@connect(
(state) => ({
issues: state.getIn(['sessions', 'current', 'issues']),
startedAt: state.getIn(['sessions', 'current', 'startedAt']),
tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible']),
}),
{ setTimelinePointer, setTimelineHoverTime }
)
export default class Timeline extends React.PureComponent {
progressRef = React.createRef();
timelineRef = React.createRef();
wasPlaying = false;
seekProgress = (e) => {
const time = this.getTime(e);
this.props.jump(time);
this.hideTimeTooltip();
};
loadAndSeek = async (e) => {
e.persist();
await toggleTimetravel();
function Timeline(props) {
const { player, store } = React.useContext(PlayerContext)
const [wasPlaying, setWasPlaying] = React.useState(false);
const { notesStore } = useStore();
const {
playing,
time,
skipIntervals,
eventList: events,
skip,
skipToIssue,
disabled,
endTime,
live,
} = store.get()
const notes = notesStore.sessionNotes
setTimeout(() => {
this.seekProgress(e);
});
};
const progressRef = React.useRef();
const timelineRef = React.useRef();
jumpToTime = (e) => {
if (this.props.live && !this.props.liveTimeTravel) {
this.loadAndSeek(e);
} else {
this.seekProgress(e);
}
};
getTime = (e, customEndTime) => {
const { endTime } = this.props;
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const targetTime = customEndTime || endTime;
const time = Math.max(Math.round(p * targetTime), 0);
const scale = 100 / endTime;
return time;
};
createEventClickHandler = (pointer) => (e) => {
e.stopPropagation();
this.props.jump(pointer.time);
this.props.setTimelinePointer(pointer);
};
componentDidMount() {
const { issues, skipToIssue } = this.props;
React.useEffect(() => {
const { issues } = props;
const firstIssue = issues.get(0);
deboucneJump = debounce(this.props.jump, 500);
debounceTooltipChange = debounce(this.props.setTimelineHoverTime, 50);
deboucneJump = debounce(player.jump, 500);
debounceTooltipChange = debounce(props.setTimelineHoverTime, 50);
if (firstIssue && skipToIssue) {
this.props.jump(firstIssue.time);
player.jump(firstIssue.time);
}
}
}, [])
onDragEnd = () => {
const { live, liveTimeTravel } = this.props;
const onDragEnd = () => {
if (live && !liveTimeTravel) return;
if (this.wasPlaying) {
this.props.togglePlay();
if (wasPlaying) {
player.togglePlay();
}
};
onDrag = (offset) => {
const { endTime, live, liveTimeTravel } = this.props;
const onDrag = (offset) => {
if (live && !liveTimeTravel) return;
const p = (offset.x - BOUNDRY) / this.progressRef.current.offsetWidth;
const p = (offset.x - BOUNDRY) / progressRef.current.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
deboucneJump(time);
this.hideTimeTooltip();
if (this.props.playing) {
this.wasPlaying = true;
this.props.pause();
hideTimeTooltip();
if (playing) {
setWasPlaying(true)
player.pause();
}
};
getLiveTime = (e) => {
const { startedAt } = this.props;
const getLiveTime = (e) => {
const duration = new Date().getTime() - startedAt;
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const time = Math.max(Math.round(p * duration), 0);
@ -126,23 +87,23 @@ export default class Timeline extends React.PureComponent {
return [time, duration];
};
showTimeTooltip = (e) => {
if (e.target !== this.progressRef.current && e.target !== this.timelineRef.current) {
return this.props.tooltipVisible && this.hideTimeTooltip();
const showTimeTooltip = (e) => {
if (e.target !== progressRef.current && e.target !== timelineRef.current) {
return props.tooltipVisible && hideTimeTooltip();
}
const { live } = this.props;
let timeLineTooltip;
if (live) {
const [time, duration] = this.getLiveTime(e);
const [time, duration] = getLiveTime(e);
timeLineTooltip = {
time: duration - time,
offset: e.nativeEvent.offsetX,
isVisible: true,
};
} else {
const time = this.getTime(e);
const time = getTime(e);
timeLineTooltip = {
time: time,
offset: e.nativeEvent.offsetX,
@ -153,18 +114,44 @@ export default class Timeline extends React.PureComponent {
debounceTooltipChange(timeLineTooltip);
};
hideTimeTooltip = () => {
const hideTimeTooltip = () => {
const timeLineTooltip = { isVisible: false };
debounceTooltipChange(timeLineTooltip);
};
render() {
const { events, skip, skipIntervals, disabled, endTime, live, notes } = this.props;
const seekProgress = (e) => {
const time = getTime(e);
player.jump(time);
hideTimeTooltip();
};
const scale = 100 / endTime;
const loadAndSeek = async (e) => {
e.persist();
await toggleTimetravel();
return (
<div
setTimeout(() => {
seekProgress(e);
});
};
const jumpToTime = (e) => {
if (live && !liveTimeTravel) {
loadAndSeek(e);
} else {
seekProgress(e);
}
};
const getTime = (e, customEndTime) => {
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const targetTime = customEndTime || endTime;
const time = Math.max(Math.round(p * targetTime), 0);
return time;
};
return (
<div
className="flex items-center absolute w-full"
style={{
top: '-4px',
@ -176,26 +163,26 @@ export default class Timeline extends React.PureComponent {
>
<div
className={stl.progress}
onClick={disabled ? null : this.jumpToTime}
ref={this.progressRef}
onClick={disabled ? null : jumpToTime}
ref={progressRef}
role="button"
onMouseMoveCapture={this.showTimeTooltip}
onMouseEnter={this.showTimeTooltip}
onMouseLeave={this.hideTimeTooltip}
onMouseMoveCapture={showTimeTooltip}
onMouseEnter={showTimeTooltip}
onMouseLeave={hideTimeTooltip}
>
<TooltipContainer live={live} />
{/* custo color is live */}
<DraggableCircle
left={this.props.time * scale}
onDrop={this.onDragEnd}
live={this.props.live}
left={time * scale}
onDrop={onDragEnd}
live={live}
/>
<CustomDragLayer
onDrag={this.onDrag}
onDrag={onDrag}
minX={BOUNDRY}
maxX={this.progressRef.current && this.progressRef.current.offsetWidth + BOUNDRY}
maxX={progressRef.current && progressRef.current.offsetWidth + BOUNDRY}
/>
<TimeTracker scale={scale} live={this.props.live} left={this.props.time * scale} />
<TimeTracker scale={scale} live={live} left={time * scale} />
{!live && skip ?
skipIntervals.map((interval) => (
@ -208,7 +195,7 @@ export default class Timeline extends React.PureComponent {
}}
/>
)) : null}
<div className={stl.timeline} ref={this.timelineRef} />
<div className={stl.timeline} ref={timelineRef} />
{events.map((e) => (
<div
@ -235,6 +222,14 @@ export default class Timeline extends React.PureComponent {
) : null)}
</div>
</div>
);
}
)
}
export default connect(
(state) => ({
issues: state.getIn(['sessions', 'current', 'issues']),
startedAt: state.getIn(['sessions', 'current', 'startedAt']),
tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible']),
}),
{ setTimelinePointer, setTimelineHoverTime }
)(observer(Timeline))

View file

@ -7,7 +7,6 @@ import { setCreateNoteTooltip, addNote, updateNote } from 'Duck/sessions';
import stl from './styles.module.css';
import { useStore } from 'App/mstore';
import { toast } from 'react-toastify';
import { injectNotes } from 'Player';
import { fetchList as fetchSlack } from 'Duck/integrations/slack';
import Select from 'Shared/Select';
import { TeamBadge } from 'Shared/SessionListContainer/components/Notes'
@ -86,7 +85,6 @@ function CreateNote({
.then((r) => {
toast.success('Note updated');
notesStore.fetchSessionNotes(sessionId).then((notes) => {
injectNotes(notes);
onSuccess(editNote.noteId)
updateNote(r);
});
@ -108,7 +106,6 @@ function CreateNote({
onSuccess(r.noteId as unknown as string)
toast.success('Note added');
notesStore.fetchSessionNotes(sessionId).then((notes) => {
injectNotes(notes);
addNote(r);
});
})

View file

@ -93,11 +93,16 @@ function PlayerControls(props: Props) {
)}
<div className="rounded ml-4 bg-active-blue border border-active-blue-border flex items-stretch">
<button
ref={arrowBackRef}
className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent"
{/* @ts-ignore */}
<Tooltip
anchorClassName="h-full hover:border-active-blue-border hover:bg-active-blue-border focus:border focus:border-blue border-borderColor-transparent"
title={`Rewind ${currentInterval}s`}
placement="top"
>
<Tooltip title="Rewind 10s">
<button
ref={arrowBackRef}
className="h-full bg-transparent"
>
{controlIcon(
'skip-forward-fill',
18,
@ -105,8 +110,8 @@ function PlayerControls(props: Props) {
true,
'hover:bg-active-blue-border color-main h-full flex items-center'
)}
</Tooltip>
</button>
</button>
</Tooltip>
<div className="p-1 border-l border-r bg-active-blue-border border-active-blue-border flex items-center">
<Popover
@ -143,19 +148,23 @@ function PlayerControls(props: Props) {
)}
>
<div onClick={toggleTooltip} ref={skipRef} className="cursor-pointer select-none">
{/* @ts-ignore */}
<Tooltip disabled={showTooltip} title="Set default skip duration">
{/* @ts-ignore */}
{currentInterval}s
</Tooltip>
</div>
</Popover>
</div>
<button
ref={arrowForwardRef}
className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent"
<Tooltip
anchorClassName="h-full hover:border-active-blue-border hover:bg-active-blue-border focus:border focus:border-blue border-borderColor-transparent"
title={`Rewind ${currentInterval}s`}
placement="top"
>
<Tooltip title="Forward 10s">
<button
ref={arrowForwardRef}
className="h-full bg-transparent"
>
{controlIcon(
'skip-forward-fill',
18,
@ -163,8 +172,8 @@ function PlayerControls(props: Props) {
false,
'hover:bg-active-blue-border color-main h-full flex items-center'
)}
</Tooltip>
</button>
</button>
</Tooltip>
</div>
{!live && (

View file

@ -1,8 +1,5 @@
import React from 'react';
import { connectPlayer } from 'Player';
import { getStatusText } from 'Player';
import type { MarkedTarget } from 'Player';
import { CallingState, ConnectionStatus, RemoteControlStatus } from 'Player';
import { CallingState, ConnectionStatus, RemoteControlStatus, getStatusText } from 'Player';
import AutoplayTimer from './Overlay/AutoplayTimer';
import PlayIconLayer from './Overlay/PlayIconLayer';
@ -10,45 +7,40 @@ import LiveStatusText from './Overlay/LiveStatusText';
import Loader from './Overlay/Loader';
import ElementsMarker from './Overlay/ElementsMarker';
import RequestingWindow, { WindowType } from 'App/components/Assist/RequestingWindow';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
interface Props {
playing: boolean,
completed: boolean,
inspectorMode: boolean,
loading: boolean,
live: boolean,
liveStatusText: string,
concetionStatus: ConnectionStatus,
autoplay: boolean,
markedTargets: MarkedTarget[] | null,
activeTargetIndex: number,
calling: CallingState,
remoteControl: RemoteControlStatus
nextId: string,
togglePlay: () => void,
closedLive?: boolean,
livePlay?: boolean,
}
function Overlay({
playing,
completed,
inspectorMode,
loading,
live,
liveStatusText,
concetionStatus,
autoplay,
markedTargets,
activeTargetIndex,
nextId,
togglePlay,
closedLive,
livePlay,
calling,
remoteControl,
}: Props) {
const { player, store } = React.useContext(PlayerContext)
const togglePlay = () => player.togglePlay()
const {
playing,
messagesLoading,
cssLoading,
completed,
autoplay,
inspectorMode,
live,
peerConnectionStatus,
markedTargets,
activeTargetIndex,
livePlay,
calling,
remoteControl,
} = store.get()
const loading = messagesLoading || cssLoading
const liveStatusText = getStatusText(peerConnectionStatus)
const concetionStatus = peerConnectionStatus
const showAutoplayTimer = !live && completed && autoplay && nextId
const showPlayIconLayer = !live && !markedTargets && !inspectorMode && !loading && !showAutoplayTimer;
const showLiveStatusText = live && livePlay && liveStatusText && !loading;
@ -74,18 +66,4 @@ function Overlay({
}
export default connectPlayer(state => ({
playing: state.playing,
loading: state.messagesLoading || state.cssLoading,
completed: state.completed,
autoplay: state.autoplay,
inspectorMode: state.inspectorMode,
live: state.live,
liveStatusText: getStatusText(state.peerConnectionStatus),
concetionStatus: state.peerConnectionStatus,
markedTargets: state.markedTargets,
activeTargetIndex: state.activeTargetIndex,
livePlay: state.livePlay,
calling: state.calling,
remoteControl: state.remoteControl,
}))(Overlay);
export default observer(Overlay);

View file

@ -2,8 +2,8 @@ import React from 'react';
import type { MarkedTarget } from 'Player';
import cn from 'classnames';
import stl from './Marker.module.css';
import { activeTarget } from 'Player';
import { Tooltip } from 'UI';
import { PlayerContext } from 'App/components/Session/playerContext';
interface Props {
target: MarkedTarget;
@ -17,11 +17,14 @@ export default function Marker({ target, active }: Props) {
width: `${target.boundingRect.width}px`,
height: `${target.boundingRect.height}px`,
};
const { player } = React.useContext(PlayerContext)
return (
<div
className={cn(stl.marker, { [stl.active]: active })}
style={style}
onClick={() => activeTarget(target.index)}
// @ts-ignore
onClick={() => player.setActiveTarget(target.index)}
>
<div className={stl.index}>{target.index + 1}</div>
<Tooltip open={active} delay={0} title={<div>{target.count} Clicks</div>}>

View file

@ -4,7 +4,6 @@ import { findDOMNode } from 'react-dom';
import cn from 'classnames';
import { EscapeButton } from 'UI';
import { hide as hideTargetDefiner } from 'Duck/components/targetDefiner';
import { fullscreenOff } from 'Duck/components/player';
import {
NONE,
CONSOLE,
@ -15,26 +14,16 @@ import {
PERFORMANCE,
GRAPHQL,
EXCEPTIONS,
LONGTASKS,
INSPECTOR,
OVERVIEW,
fullscreenOff,
} from 'Duck/components/player';
import NetworkPanel from 'Shared/DevTools/NetworkPanel';
import Console from '../Console/Console';
import StackEvents from '../StackEvents/StackEvents';
import Storage from '../Storage';
import Profiler from '../Profiler';
import { ConnectedPerformance } from '../Performance';
import GraphQL from '../GraphQL';
import Exceptions from '../Exceptions/Exceptions';
import LongTasks from '../LongTasks';
import Inspector from '../Inspector';
import {
attach as attachPlayer,
Controls as PlayerControls,
scale as scalePlayerScreen,
connectPlayer,
} from 'Player';
import Controls from './Controls';
import Overlay from './Overlay';
import stl from './player.module.css';
@ -42,72 +31,49 @@ import { updateLastPlayedSession } from 'Duck/sessions';
import OverviewPanel from '../OverviewPanel';
import ConsolePanel from 'Shared/DevTools/ConsolePanel';
import ProfilerPanel from 'Shared/DevTools/ProfilerPanel';
import { PlayerContext } from 'App/components/Session/playerContext';
import StackEventPanel from 'Shared/DevTools/StackEventPanel';
@connectPlayer((state) => ({
live: state.live,
}))
@connect(
(state) => {
const isAssist = window.location.pathname.includes('/assist/');
return {
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
nextId: state.getIn(['sessions', 'nextId']),
sessionId: state.getIn(['sessions', 'current', 'sessionId']),
closedLive:
!!state.getIn(['sessions', 'errors']) ||
(isAssist && !state.getIn(['sessions', 'current', 'live'])),
};
},
{
hideTargetDefiner,
function Player(props) {
const {
className,
fullscreen,
fullscreenOff,
updateLastPlayedSession,
}
)
export default class Player extends React.PureComponent {
screenWrapper = React.createRef();
nextId,
closedLive,
bottomBlock,
activeTab,
fullView,
} = props;
const playerContext = React.useContext(PlayerContext)
const screenWrapper = React.useRef();
const bottomBlockIsActive = !fullscreen && bottomBlock !== NONE
componentDidUpdate(prevProps) {
if (
[prevProps.bottomBlock, this.props.bottomBlock].includes(NONE) ||
prevProps.fullscreen !== this.props.fullscreen
) {
scalePlayerScreen();
React.useEffect(() => {
props.updateLastPlayedSession(props.sessionId);
if (!props.closedLive) {
const parentElement = findDOMNode(screenWrapper.current); //TODO: good architecture
playerContext.player.attach(parentElement);
}
}
componentDidMount() {
this.props.updateLastPlayedSession(this.props.sessionId);
if (this.props.closedLive) return;
}, [])
const parentElement = findDOMNode(this.screenWrapper.current); //TODO: good architecture
attachPlayer(parentElement);
}
React.useEffect(() => {
playerContext.player.scale();
}, [props.bottomBlock, props.fullscreen, playerContext.player])
render() {
const {
className,
bottomBlockIsActive,
fullscreen,
fullscreenOff,
nextId,
closedLive,
bottomBlock,
activeTab,
fullView = false,
} = this.props;
if (!playerContext.player) return null;
const maxWidth = activeTab ? 'calc(100vw - 270px)' : '100vw';
return (
<div
const maxWidth = activeTab ? 'calc(100vw - 270px)' : '100vw';
return (
<div
className={cn(className, stl.playerBody, 'flex flex-col relative', fullscreen && 'pb-2')}
data-bottom-block={bottomBlockIsActive}
>
{fullscreen && <EscapeButton onClose={fullscreenOff} />}
<div className="relative flex-1 overflow-hidden">
<Overlay nextId={nextId} togglePlay={PlayerControls.togglePlay} closedLive={closedLive} />
<div className={stl.screenWrapper} ref={this.screenWrapper} />
<Overlay nextId={nextId} closedLive={closedLive} />
<div className={stl.screenWrapper} ref={screenWrapper} />
</div>
{!fullscreen && !!bottomBlock && (
<div style={{ maxWidth, width: '100%' }}>
@ -124,12 +90,33 @@ export default class Player extends React.PureComponent {
{bottomBlock === PERFORMANCE && <ConnectedPerformance />}
{bottomBlock === GRAPHQL && <GraphQL />}
{bottomBlock === EXCEPTIONS && <Exceptions />}
{bottomBlock === LONGTASKS && <LongTasks />}
{bottomBlock === INSPECTOR && <Inspector />}
</div>
)}
{!fullView && <Controls {...PlayerControls} /> }
{!fullView && <Controls
speedDown={playerContext.player.speedDown}
speedUp={playerContext.player.speedUp}
jump={playerContext.player.jump}
/>}
</div>
);
}
)
}
export default connect((state) => {
const isAssist = window.location.pathname.includes('/assist/');
return {
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
nextId: state.getIn(['sessions', 'nextId']),
sessionId: state.getIn(['sessions', 'current', 'sessionId']),
bottomBlock: state.getIn(['components', 'player', 'bottomBlock']),
closedLive:
!!state.getIn(['sessions', 'errors']) ||
(isAssist && !state.getIn(['sessions', 'current', 'live'])),
};
},
{
hideTargetDefiner,
fullscreenOff,
updateLastPlayedSession,
}
)(Player)

View file

@ -1,7 +1,6 @@
import React from 'react';
import cn from 'classnames';
import { connect } from 'react-redux';
import { NONE } from 'Duck/components/player';
import Player from './Player';
import SubHeader from './Subheader';
@ -9,14 +8,13 @@ import styles from './playerBlock.module.css';
@connect((state) => ({
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
bottomBlock: state.getIn(['components', 'player', 'bottomBlock']),
sessionId: state.getIn(['sessions', 'current', 'sessionId']),
disabled: state.getIn(['components', 'targetDefiner', 'inspectorMode']),
jiraConfig: state.getIn(['issues', 'list']).first(),
}))
export default class PlayerBlock extends React.PureComponent {
render() {
const { fullscreen, bottomBlock, sessionId, disabled, activeTab, jiraConfig, fullView = false } = this.props;
const { fullscreen, sessionId, disabled, activeTab, jiraConfig, fullView = false } = this.props;
return (
<div className={cn(styles.playerBlock, 'flex flex-col overflow-x-hidden')}>
@ -25,9 +23,7 @@ export default class PlayerBlock extends React.PureComponent {
)}
<Player
className="flex-1"
bottomBlockIsActive={!fullscreen && bottomBlock !== NONE}
// bottomBlockIsActive={ true }
bottomBlock={bottomBlock}
fullscreen={fullscreen}
activeTab={activeTab}
fullView={fullView}

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

@ -33,7 +33,7 @@ export default class Profiler extends React.PureComponent {
return (
<React.Fragment>
<SlideModal
<SlideModal
title={ modalProfile && modalProfile.name }
isDisplayed={ modalProfile !== null }
content={ modalProfile && <ProfileInfo profile={ modalProfile } />}
@ -55,7 +55,7 @@ export default class Profiler extends React.PureComponent {
/>
</BottomBlock.Header>
<BottomBlock.Content>
<TimeTable
<TimeTable
rows={ filteredProfiles }
onRowClick={ this.onProfileClick }
hoverable

View file

@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { setAutoplayValues } from 'Duck/sessions';
import { session as sessionRoute } from 'App/routes';
import { Link, Icon, Tooltip } from 'UI';
import { Link, Icon, Tooltip } from 'UI';;
import { withRouter, RouteComponentProps } from 'react-router-dom';
import cn from 'classnames';
import { fetchAutoplaySessions } from 'Duck/search';
@ -20,7 +20,7 @@ interface Props extends RouteComponentProps {
sessionIds: any;
fetchAutoplaySessions?: (page: number) => Promise<void>;
}
function Autoplay(props: Props) {
function QueueControls(props: Props) {
const {
previousId,
nextId,
@ -30,9 +30,10 @@ function Autoplay(props: Props) {
latestRequestTime,
match: {
// @ts-ignore
params: { siteId, sessionId },
params: { sessionId },
},
} = props;
const disabled = sessionIds.length === 0;
useEffect(() => {
@ -99,4 +100,4 @@ export default connect(
latestRequestTime: state.getIn(['search', 'latestRequestTime']),
}),
{ setAutoplayValues, fetchAutoplaySessions }
)(withRouter(Autoplay));
)(withRouter(QueueControls))

View file

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

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

@ -0,0 +1,308 @@
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: 3 }} 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 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={list.length === 0}
>
{showStore && (
<div className="ph-10 scroll-y" style={{ width: '25%' }}>
{list.length === 0 ? (
<div className="color-gray-light font-size-16 mt-20 text-center">
{'Empty state.'}
</div>
) : (
<JSONTree collapsed={2} src={listNow.length === 0 ? list[0].state : listNow[listNow.length - 1].state} />
)}
</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

@ -1,45 +1,56 @@
import React from 'react';
import { Icon, Tooltip, Button } from 'UI';
import Autoplay from './Autoplay';
import QueueControls from './QueueControls';
import Bookmark from 'Shared/Bookmark';
import SharePopup from '../shared/SharePopup/SharePopup';
import copy from 'copy-to-clipboard';
import Issues from './Issues/Issues';
import NotePopup from './components/NotePopup';
import { connectPlayer, pause } from 'Player';
import ItemMenu from './components/HeaderMenu';
import { useModal } from 'App/components/Modal';
import BugReportModal from './BugReport/BugReportModal';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import AutoplayToggle from 'Shared/AutoplayToggle';
function SubHeader(props) {
const { player, store } = React.useContext(PlayerContext)
const {
width,
height,
location: currentLocation,
fetchList,
graphqlList,
resourceList,
exceptionsList,
eventList: eventsList,
endTime,
} = store.get()
const mappedResourceList = resourceList
.filter((r) => r.isRed() || r.isYellow())
.concat(fetchList.filter((i) => parseInt(i.status) >= 400))
.concat(graphqlList.filter((i) => parseInt(i.status) >= 400))
const [isCopied, setCopied] = React.useState(false);
const { showModal, hideModal } = useModal();
const isAssist = window.location.pathname.includes('/assist/');
const location =
props.currentLocation && props.currentLocation.length > 60
? `${props.currentLocation.slice(0, 60)}...`
: props.currentLocation;
currentLocation && currentLocation.length > 60
? `${currentLocation.slice(0, 60)}...`
: currentLocation;
const showReportModal = () => {
pause();
player.pause();
const xrayProps = {
currentLocation: props.currentLocation,
resourceList: props.resourceList,
exceptionsList: props.exceptionsList,
eventsList: props.eventsList,
endTime: props.endTime,
};
showModal(
<BugReportModal
width={props.width}
height={props.height}
xrayProps={xrayProps}
hideModal={hideModal}
/>,
{ right: true }
);
currentLocation: currentLocation,
resourceList: mappedResourceList,
exceptionsList: exceptionsList,
eventsList: eventsList,
endTime: endTime,
}
showModal(<BugReportModal width={width} height={height} xrayProps={xrayProps} hideModal={hideModal} />, { right: true });
};
return (
@ -48,7 +59,7 @@ function SubHeader(props) {
<div
className="flex items-center cursor-pointer color-gray-medium text-sm p-1 hover:bg-gray-light-shade rounded-md"
onClick={() => {
copy(props.currentLocation);
copy(currentLocation);
setCopied(true);
setTimeout(() => setCopied(false), 5000);
}}
@ -95,7 +106,7 @@ function SubHeader(props) {
/>
<div>
<Autoplay />
<QueueControls />
</div>
</div>
) : null}
@ -103,17 +114,4 @@ function SubHeader(props) {
);
}
const SubH = connectPlayer((state) => ({
width: state.width,
height: state.height,
currentLocation: state.location,
resourceList: state.resourceList
.filter((r) => r.isRed() || r.isYellow())
.concat(state.fetchList.filter((i) => parseInt(i.status) >= 400))
.concat(state.graphqlList.filter((i) => parseInt(i.status) >= 400)),
exceptionsList: state.exceptionsList,
eventsList: state.eventList,
endTime: state.endTime,
}))(SubHeader);
export default React.memo(SubH);
export default observer(SubHeader);

View file

@ -1,23 +1,23 @@
import React from 'react';
import { Button } from 'UI';
import { connectPlayer, pause } from 'Player';
import { connect } from 'react-redux';
import { setCreateNoteTooltip } from 'Duck/sessions';
import GuidePopup, { FEATURE_KEYS } from 'Shared/GuidePopup';
import GuidePopup from 'Shared/GuidePopup';
import { PlayerContext } from 'App/components/Session/playerContext';
function NotePopup({
setCreateNoteTooltip,
time,
tooltipActive,
}: {
setCreateNoteTooltip: (args: any) => void;
time: number;
tooltipActive: boolean;
}) {
const { player, store } = React.useContext(PlayerContext)
const toggleNotePopup = () => {
if (tooltipActive) return;
pause();
setCreateNoteTooltip({ time: time, isVisible: true });
player.pause();
setCreateNoteTooltip({ time: store.get().time, isVisible: true });
};
React.useEffect(() => {
@ -36,14 +36,9 @@ function NotePopup({
);
}
const NotePopupPl = connectPlayer(
// @ts-ignore
(state) => ({ time: state.time })
)(React.memo(NotePopup));
const NotePopupComp = connect(
(state: any) => ({ tooltipActive: state.getIn(['sessions', 'createNoteTooltip', 'isVisible']) }),
{ setCreateNoteTooltip }
)(NotePopupPl);
)(NotePopup);
export default React.memo(NotePopupComp);

View file

@ -1,16 +1,19 @@
import React from 'react';
import { Controls as PlayerControls, connectPlayer } from 'Player';
import { Toggler } from 'UI';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
interface Props {
toggleAutoplay: () => void;
autoplay: boolean;
}
function AutoplayToggle(props: Props) {
const { autoplay } = props;
const { player, store } = React.useContext(PlayerContext)
const { autoplay } = store.get()
return (
<div
onClick={props.toggleAutoplay}
onClick={() => player.toggleAutoplay()}
className="cursor-pointer flex items-center mr-2 hover:bg-gray-light-shade rounded-md p-2"
>
<Toggler name="sessionsLive" onChange={props.toggleAutoplay} checked={autoplay} />
@ -19,11 +22,4 @@ function AutoplayToggle(props: Props) {
);
}
export default connectPlayer(
(state: any) => ({
autoplay: state.autoplay,
}),
{
toggleAutoplay: PlayerControls.toggleAutoplay,
}
)(AutoplayToggle);
export default observer(AutoplayToggle);

View file

@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import cn from 'classnames';
import { closeBottomBlock } from 'Duck/components/player';
import { Input, CloseButton } from 'UI';
import { CloseButton } from 'UI';
import stl from './header.module.css';
const Header = ({

View file

@ -1,9 +0,0 @@
// import { NONE, CONSOLE, NETWORK, STACKEVENTS, REDUX_STATE, PROFILER, PERFORMANCE, GRAPHQL } from 'Duck/components/player';
//
//
// export default {
// [NONE]: {
// Component: null,
//
// }
// }

View file

@ -1,17 +1,18 @@
import React, { useEffect, useRef, useState } from 'react';
import { connectPlayer, jump } from 'Player';
import Log from 'Types/session/log';
import React, { useEffect, useRef, useState, useMemo } from 'react';
import { LogLevel, ILog } from 'Player';
import BottomBlock from '../BottomBlock';
import { LEVEL } from 'Types/session/log';
import { Tabs, Input, Icon, NoContent } from 'UI';
import cn from 'classnames';
import ConsoleRow from '../ConsoleRow';
import { getRE } from 'App/utils';
import { List, CellMeasurer, CellMeasurerCache, AutoSizer } from 'react-virtualized';
import { useObserver } from 'mobx-react-lite';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { List, CellMeasurer, AutoSizer } from 'react-virtualized';
import { useStore } from 'App/mstore';
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
import { useModal } from 'App/components/Modal';
import useAutoscroll from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'
import useCellMeasurerCache from '../useCellMeasurerCache'
const ALL = 'ALL';
const INFO = 'INFO';
@ -19,35 +20,34 @@ const WARNINGS = 'WARNINGS';
const ERRORS = 'ERRORS';
const LEVEL_TAB = {
[LEVEL.INFO]: INFO,
[LEVEL.LOG]: INFO,
[LEVEL.WARNING]: WARNINGS,
[LEVEL.ERROR]: ERRORS,
[LEVEL.EXCEPTION]: ERRORS,
};
[LogLevel.INFO]: INFO,
[LogLevel.LOG]: INFO,
[LogLevel.WARN]: WARNINGS,
[LogLevel.ERROR]: ERRORS,
[LogLevel.EXCEPTION]: ERRORS,
} as const
const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({ text: tab, key: tab }));
function renderWithNL(s = '') {
if (typeof s !== 'string') return '';
return s.split('\n').map((line, i) => <div className={cn({ 'ml-20': i !== 0 })}>{line}</div>);
return s.split('\n').map((line, i) => <div key={i + line.slice(0, 6)} className={cn({ 'ml-20': i !== 0 })}>{line}</div>);
}
const getIconProps = (level: any) => {
switch (level) {
case LEVEL.INFO:
case LEVEL.LOG:
case LogLevel.INFO:
case LogLevel.LOG:
return {
name: 'console/info',
color: 'blue2',
};
case LEVEL.WARN:
case LEVEL.WARNING:
case LogLevel.WARN:
return {
name: 'console/warning',
color: 'red2',
};
case LEVEL.ERROR:
case LogLevel.ERROR:
return {
name: 'console/error',
color: 'red',
@ -56,126 +56,53 @@ const getIconProps = (level: any) => {
return null;
};
const INDEX_KEY = 'console';
let timeOut: any = null;
const TIMEOUT_DURATION = 5000;
interface Props {
logs: any;
exceptions: any;
time: any;
}
function ConsolePanel(props: Props) {
const { logs, time } = props;
const additionalHeight = 0;
// const [activeTab, setActiveTab] = useState(ALL);
// const [filter, setFilter] = useState('');
function ConsolePanel() {
const {
sessionStore: { devTools },
} = useStore();
const [filteredList, setFilteredList] = useState([]);
const filter = useObserver(() => devTools[INDEX_KEY].filter);
const activeTab = useObserver(() => devTools[INDEX_KEY].activeTab);
const activeIndex = useObserver(() => devTools[INDEX_KEY].index);
const [pauseSync, setPauseSync] = useState(activeIndex > 0);
const synRef: any = useRef({});
const { showModal, component: modalActive } = useModal();
} = useStore()
const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab });
const onFilterChange = ({ target: { value } }: any) => {
devTools.update(INDEX_KEY, { filter: value });
};
const filter = devTools[INDEX_KEY].filter;
const activeTab = devTools[INDEX_KEY].activeTab;
// Why do we need to keep index in the store? if we could get read of it it would simplify the code
const activeIndex = devTools[INDEX_KEY].index;
const [ isDetailsModalActive, setIsDetailsModalActive ] = useState(false);
const { showModal } = useModal();
synRef.current = {
pauseSync,
const { player, store } = React.useContext(PlayerContext)
const jump = (t: number) => player.jump(t)
const { logList, exceptionsList, logListNow, exceptionsListNow } = store.get()
const list = useMemo(() =>
logList.concat(exceptionsList).sort((a, b) => a.time - b.time),
[ logList.length, exceptionsList.length ],
) as ILog[]
let filteredList = useRegExListFilterMemo(list, l => l.value, filter)
filteredList = useTabListFilterMemo(filteredList, l => LEVEL_TAB[l.level], ALL, activeTab)
const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab })
const onFilterChange = ({ target: { value } }: any) => devTools.update(INDEX_KEY, { filter: value })
// AutoScroll
const countNow = logListNow.length + exceptionsListNow.length
const [
timeoutStartAutoscroll,
stopAutoscroll,
] = useAutoscroll(
filteredList,
list[countNow].time,
activeIndex,
};
const removePause = () => {
if (!!modalActive) return;
clearTimeout(timeOut);
timeOut = setTimeout(() => {
devTools.update(INDEX_KEY, { index: getCurrentIndex() });
setPauseSync(false);
}, TIMEOUT_DURATION);
};
index => devTools.update(INDEX_KEY, { index })
)
const onMouseEnter = stopAutoscroll
const onMouseLeave = () => {
removePause();
};
useEffect(() => {
if (pauseSync) {
removePause();
}
return () => {
clearTimeout(timeOut);
if (!synRef.current.pauseSync) {
devTools.update(INDEX_KEY, { index: 0 });
}
};
}, []);
const getCurrentIndex = () => {
return filteredList.filter((item: any) => item.time <= time).length - 1;
};
useEffect(() => {
const currentIndex = getCurrentIndex();
if (currentIndex !== activeIndex && !pauseSync) {
devTools.update(INDEX_KEY, { index: currentIndex });
}
}, [time]);
const cache = new CellMeasurerCache({
fixedWidth: true,
keyMapper: (index: number) => filteredList[index],
});
const _list = React.useRef();
const showDetails = (log: any) => {
clearTimeout(timeOut);
showModal(<ErrorDetailsModal errorId={log.errorId} />, { right: true, onClose: removePause });
devTools.update(INDEX_KEY, { index: filteredList.indexOf(log) });
setPauseSync(true);
};
const _rowRenderer = ({ index, key, parent, style }: any) => {
const item = filteredList[index];
return (
// @ts-ignore
<CellMeasurer cache={cache} columnIndex={0} key={key} rowIndex={index} parent={parent}>
{({ measure }: any) => (
<ConsoleRow
style={style}
log={item}
jump={jump}
iconProps={getIconProps(item.level)}
renderWithNL={renderWithNL}
onClick={() => showDetails(item)}
recalcHeight={() => {
measure();
(_list as any).current.recomputeRowHeights(index);
}}
/>
)}
</CellMeasurer>
);
};
React.useMemo(() => {
const filterRE = getRE(filter, 'i');
let list = logs;
list = list.filter(
({ value, level }: any) =>
(!!filter ? filterRE.test(value) : true) &&
(activeTab === ALL || activeTab === LEVEL_TAB[level])
);
setFilteredList(list);
}, [logs, filter, activeTab]);
if (isDetailsModalActive) { return }
timeoutStartAutoscroll()
}
const _list = useRef(); // TODO: fix react-virtualized types & incapsulate scrollToRow logic
useEffect(() => {
if (_list.current) {
// @ts-ignore
@ -183,10 +110,52 @@ function ConsolePanel(props: Props) {
}
}, [activeIndex]);
const cache = useCellMeasurerCache(filteredList)
const showDetails = (log: any) => {
setIsDetailsModalActive(true);
showModal(
<ErrorDetailsModal errorId={log.errorId} />,
{
right: true,
onClose: () => {
setIsDetailsModalActive(false)
timeoutStartAutoscroll()
}
});
devTools.update(INDEX_KEY, { index: filteredList.indexOf(log) });
stopAutoscroll()
}
const _rowRenderer = ({ index, key, parent, style }: any) => {
const item = filteredList[index];
return (
<React.Fragment key={key}>
{/* @ts-ignore */}
<CellMeasurer cache={cache} columnIndex={0} key={key} rowIndex={index} parent={parent}>
{({ measure }: any) => (
<ConsoleRow
style={style}
log={item}
jump={jump}
iconProps={getIconProps(item.level)}
renderWithNL={renderWithNL}
onClick={() => showDetails(item)}
recalcHeight={() => {
measure();
(_list as any).current.recomputeRowHeights(index);
}}
/>
)}
</CellMeasurer>
</React.Fragment>
)
}
return (
<BottomBlock
style={{ height: 300 + additionalHeight + 'px' }}
onMouseEnter={() => setPauseSync(true)}
style={{ height: '300px' }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{/* @ts-ignore */}
@ -199,7 +168,6 @@ function ConsolePanel(props: Props) {
className="input-small h-8"
placeholder="Filter by keyword"
icon="search"
iconPosition="left"
name="filter"
height={28}
onChange={onFilterChange}
@ -244,19 +212,4 @@ function ConsolePanel(props: Props) {
);
}
export default connectPlayer((state: any) => {
const logs = state.logList;
const exceptions = state.exceptionsList; // TODO merge
const logExceptions = exceptions.map(({ time, errorId, name, projectId }: any) =>
Log({
level: LEVEL.ERROR,
value: name,
time,
errorId,
})
);
return {
time: state.time,
logs: logs.concat(logExceptions),
};
})(ConsolePanel);
export default observer(ConsolePanel);

View file

@ -15,7 +15,7 @@ interface Props {
function ConsoleRow(props: Props) {
const { log, iconProps, jump, renderWithNL, style, recalcHeight } = props;
const [expanded, setExpanded] = useState(false);
const lines = log.value.split('\n').filter((l: any) => !!l);
const lines = log.value?.split('\n').filter((l: any) => !!l) || [];
const canExpand = lines.length > 1;
const clickable = canExpand || !!log.errorId;
@ -37,7 +37,7 @@ function ConsoleRow(props: Props) {
'cursor-pointer underline decoration-dotted decoration-gray-200': !!log.errorId,
}
)}
onClick={clickable ? () => (!!log.errorId ? props.onClick() : toggleExpand()) : () => {}}
onClick={clickable ? () => (!!log.errorId ? props.onClick() : toggleExpand()) : undefined}
>
<div className="mr-2">
<Icon size="14" {...iconProps} />

View file

@ -1,19 +1,21 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { useMemo, useState } from 'react';
import { observer } from 'mobx-react-lite';
import { Duration } from 'luxon';
import { Tooltip, Tabs, Input, NoContent, Icon, Toggler } from 'UI';
import { getRE } from 'App/utils';
import Resource, { TYPES } from 'Types/session/resource';
import { TYPES } from 'Types/session/resource';
import { formatBytes } from 'App/utils';
import { formatMs } from 'App/date';
import { useModal } from 'App/components/Modal';
import FetchDetailsModal from 'Shared/FetchDetailsModal';
import { PlayerContext } from 'App/components/Session/playerContext';
import { useStore } from 'App/mstore';
import TimeTable from '../TimeTable';
import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
import { Duration } from 'luxon';
import { connectPlayer, jump } from 'Player';
import { useModal } from 'App/components/Modal';
import FetchDetailsModal from 'Shared/FetchDetailsModal';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import useAutoscroll from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'
const INDEX_KEY = 'network';
@ -25,15 +27,17 @@ const IMG = 'img';
const MEDIA = 'media';
const OTHER = 'other';
const TAB_TO_TYPE_MAP: any = {
[XHR]: TYPES.XHR,
[JS]: TYPES.JS,
[CSS]: TYPES.CSS,
[IMG]: TYPES.IMG,
[MEDIA]: TYPES.MEDIA,
[OTHER]: TYPES.OTHER,
};
const TABS: any = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER].map((tab) => ({
const TYPE_TO_TAB = {
[TYPES.XHR]: XHR,
[TYPES.JS]: JS,
[TYPES.CSS]: CSS,
[TYPES.IMG]: IMG,
[TYPES.MEDIA]: MEDIA,
[TYPES.OTHER]: OTHER,
}
const TAP_KEYS = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER] as const;
const TABS = TAP_KEYS.map((tab) => ({
text: tab === 'xhr' ? 'Fetch/XHR' : tab,
key: tab,
}));
@ -80,8 +84,6 @@ function renderSize(r: any) {
content = 'Not captured';
} else {
const headerSize = r.headerSize || 0;
const encodedSize = r.encodedBodySize || 0;
const transferred = headerSize + encodedSize;
const showTransferred = r.headerSize != null;
triggerText = formatBytes(r.decodedBodySize);
@ -125,119 +127,88 @@ export function renderDuration(r: any) {
);
}
let timeOut: any = null;
const TIMEOUT_DURATION = 5000;
function NetworkPanel() {
const { player, store } = React.useContext(PlayerContext)
interface Props {
location: any;
resources: any;
fetchList: any;
domContentLoadedTime: any;
loadTime: any;
playing: boolean;
domBuildingTime: any;
time: any;
}
function NetworkPanel(props: Props) {
const { resources, time, domContentLoadedTime, loadTime, domBuildingTime, fetchList } = props;
const { showModal, component: modalActive } = useModal();
const [filteredList, setFilteredList] = useState([]);
const {
domContentLoadedTime,
loadTime,
domBuildingTime,
fetchList,
resourceList,
fetchListNow,
resourceListNow,
} = store.get()
const { showModal } = useModal();
const [sortBy, setSortBy] = useState('time');
const [sortAscending, setSortAscending] = useState(true);
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
const additionalHeight = 0;
const fetchPresented = fetchList.length > 0;
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
const {
sessionStore: { devTools },
} = useStore();
// const [filter, setFilter] = useState(devTools[INDEX_KEY].filter);
// const [activeTab, setActiveTab] = useState(ALL);
const filter = useObserver(() => devTools[INDEX_KEY].filter);
const activeTab = useObserver(() => devTools[INDEX_KEY].activeTab);
const activeIndex = useObserver(() => devTools[INDEX_KEY].index);
const [pauseSync, setPauseSync] = useState(activeIndex > 0);
const synRef: any = useRef({});
const filter = devTools[INDEX_KEY].filter;
const activeTab = devTools[INDEX_KEY].activeTab;
const activeIndex = devTools[INDEX_KEY].index;
const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab });
const onFilterChange = ({ target: { value } }: any) => {
devTools.update(INDEX_KEY, { filter: value });
};
const { list, intersectedCount } = useMemo(() => {
let intersectedCount = 0
const list = resourceList.filter(res => !fetchList.some(ft => {
if (res.url !== ft.url) { return false }
if (Math.abs(res.time - ft.time) > 200) { return false } // TODO: find good epsilons
if (Math.abs(res.duration - ft.duration) > 100) { return false }
intersectedCount++
return true
}))
.concat(fetchList)
.sort((a, b) => a.time - b.time)
return { list, intersectedCount }
}, [ resourceList.length, fetchList.length ])
synRef.current = {
pauseSync,
let filteredList = useMemo(() => {
if (!showOnlyErrors) { return list }
return list.filter(it => parseInt(it.status) >= 400 || !it.success)
}, [ showOnlyErrors, list ])
filteredList = useRegExListFilterMemo(
filteredList,
it => [ it.status, it.name, it.type ],
filter,
)
filteredList = useTabListFilterMemo(filteredList, it => TYPE_TO_TAB[it.type], ALL, activeTab)
const onTabClick = (activeTab: typeof TAP_KEYS[number]) => devTools.update(INDEX_KEY, { activeTab })
const onFilterChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => devTools.update(INDEX_KEY, { filter: value })
// AutoScroll
const countNow = fetchListNow.length + resourceListNow.length - intersectedCount
const [
timeoutStartAutoscroll,
stopAutoscroll,
] = useAutoscroll(
filteredList,
list[countNow].time,
activeIndex,
};
const removePause = () => {
if (!!modalActive) return;
clearTimeout(timeOut);
timeOut = setTimeout(() => {
devTools.update(INDEX_KEY, { index: getCurrentIndex() });
setPauseSync(false);
}, TIMEOUT_DURATION);
};
index => devTools.update(INDEX_KEY, { index })
)
const onMouseEnter = stopAutoscroll
const onMouseLeave = () => {
if (!!modalActive) return;
removePause();
};
if (isDetailsModalActive) { return }
timeoutStartAutoscroll()
}
useEffect(() => {
if (pauseSync) {
removePause();
}
return () => {
clearTimeout(timeOut);
if (!synRef.current.pauseSync) {
devTools.update(INDEX_KEY, { index: 0 });
}
};
}, []);
const getCurrentIndex = () => {
return filteredList.filter((item: any) => item.time <= time).length - 1;
};
useEffect(() => {
const currentIndex = getCurrentIndex();
if (currentIndex !== activeIndex && !pauseSync) {
devTools.update(INDEX_KEY, { index: currentIndex });
}
}, [time]);
const { resourcesSize, transferredSize } = useMemo(() => {
const resourcesSize = resources.reduce(
(sum: any, { decodedBodySize }: any) => sum + (decodedBodySize || 0),
0
);
const transferredSize = resources.reduce(
(sum: any, { headerSize, encodedBodySize }: any) =>
const resourcesSize = useMemo(() =>
resourceList.reduce(
(sum, { decodedBodySize }) => sum + (decodedBodySize || 0),
0,
), [ resourceList.length ])
const transferredSize = useMemo(() =>
resourceList.reduce(
(sum, { headerSize, encodedBodySize }) =>
sum + (headerSize || 0) + (encodedBodySize || 0),
0
);
return {
resourcesSize,
transferredSize,
};
}, [resources]);
0,
), [ resourceList.length ])
useEffect(() => {
const filterRE = getRE(filter, 'i');
let list = resources;
fetchList.forEach(
(fetchCall: any) =>
(list = list.filter((networkCall: any) => networkCall.url !== fetchCall.url))
);
list = list.concat(fetchList);
list = list.filter(
({ type, name, status, success }: any) =>
(!!filter ? filterRE.test(status) || filterRE.test(name) || filterRE.test(type) : true) &&
(activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab]) &&
(showOnlyErrors ? parseInt(status) >= 400 || !success : true)
);
setFilteredList(list);
}, [resources, filter, showOnlyErrors, activeTab]);
const referenceLines = useMemo(() => {
const arr = [];
@ -256,31 +227,30 @@ function NetworkPanel(props: Props) {
}
return arr;
}, []);
}, [ domContentLoadedTime, loadTime ])
const showDetailsModal = (row: any) => {
clearTimeout(timeOut);
setPauseSync(true);
const showDetailsModal = (item: any) => {
setIsDetailsModalActive(true)
showModal(
<FetchDetailsModal resource={row} rows={filteredList} fetchPresented={fetchPresented} />,
<FetchDetailsModal resource={item} rows={filteredList} fetchPresented={fetchList.length > 0} />,
{
right: true,
onClose: removePause,
onClose: () => {
setIsDetailsModalActive(false)
timeoutStartAutoscroll()
}
}
);
devTools.update(INDEX_KEY, { index: filteredList.indexOf(row) });
};
useEffect(() => {
devTools.update(INDEX_KEY, { filter, activeTab });
}, [filter, activeTab]);
)
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) })
stopAutoscroll()
}
return (
<React.Fragment>
<BottomBlock
style={{ height: 300 + additionalHeight + 'px' }}
style={{ height: '300px' }}
className="border"
onMouseEnter={() => setPauseSync(true)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<BottomBlock.Header>
@ -298,7 +268,6 @@ function NetworkPanel(props: Props) {
className="input-small"
placeholder="Filter by name, type or value"
icon="search"
iconPosition="left"
name="filter"
onChange={onFilterChange}
height={28}
@ -362,11 +331,11 @@ function NetworkPanel(props: Props) {
referenceLines={referenceLines}
renderPopup
onRowClick={showDetailsModal}
additionalHeight={additionalHeight}
sortBy={sortBy}
sortAscending={sortAscending}
onJump={(row: any) => {
setPauseSync(true);
devTools.update(INDEX_KEY, { index: filteredList.indexOf(row) });
jump(row.time);
player.jump(row.time);
}}
activeIndex={activeIndex}
>
@ -415,15 +384,4 @@ function NetworkPanel(props: Props) {
);
}
export default connectPlayer((state: any) => ({
location: state.location,
resources: state.resourceList,
fetchList: state.fetchList.map((i: any) =>
Resource({ ...i.toJS(), type: TYPES.XHR, time: i.time < 0 ? 0 : i.time })
),
domContentLoadedTime: state.domContentLoadedTime,
loadTime: state.loadTime,
time: state.time,
playing: state.playing,
domBuildingTime: state.domBuildingTime,
}))(NetworkPanel);
export default observer(NetworkPanel);

View file

@ -1,33 +1,27 @@
import React, { useState } from 'react';
import { connectPlayer } from 'Player';
import { observer } from 'mobx-react-lite';
import { TextEllipsis, Input } from 'UI';
import { getRE } from 'App/utils';
import { PlayerContext } from 'App/components/Session/playerContext';
import useInputState from 'App/hooks/useInputState'
// import ProfileInfo from './ProfileInfo';
import TimeTable from '../TimeTable';
import BottomBlock from '../BottomBlock';
import { useModal } from 'App/components/Modal';
import ProfilerModal from '../ProfilerModal';
import { useRegExListFilterMemo } from '../useListFilter'
const renderDuration = (p: any) => `${p.duration}ms`;
const renderName = (p: any) => <TextEllipsis text={p.name} />;
interface Props {
profiles: any;
}
function ProfilerPanel(props: Props) {
const { profiles } = props;
function ProfilerPanel() {
const { store } = React.useContext(PlayerContext)
const profiles = store.get().profilesList as any[] // TODO lest internal types
const { showModal } = useModal();
const [filter, setFilter] = useState('');
const filtered: any = React.useMemo(() => {
const filterRE = getRE(filter, 'i');
let list = profiles;
const [ filter, onFilterChange ] = useInputState()
const filtered = useRegExListFilterMemo(profiles, pr => pr.name, filter)
list = list.filter(({ name }: any) => (!!filter ? filterRE.test(name) : true));
return list;
}, [filter]);
const onFilterChange = ({ target: { value } }: any) => setFilter(value);
const onRowClick = (profile: any) => {
showModal(<ProfilerModal profile={profile} />, { right: true });
};
@ -68,8 +62,4 @@ function ProfilerPanel(props: Props) {
);
}
export default connectPlayer((state: any) => {
return {
profiles: state.profilesList,
};
})(ProfilerPanel);
export default observer(ProfilerPanel);

View file

@ -1,105 +1,89 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { hideHint } from 'Duck/components/player';
import { Tabs, Input, NoContent, Icon } from 'UI';
import { getRE } from 'App/utils';
import { List, CellMeasurer, CellMeasurerCache, AutoSizer } from 'react-virtualized';
import { observer } from 'mobx-react-lite';
import { Tooltip, Tabs, Input, NoContent, Icon, Toggler } from 'UI';
import { List, CellMeasurer, AutoSizer } from 'react-virtualized';
import { PlayerContext } from 'App/components/Session/playerContext';
import BottomBlock from '../BottomBlock';
import { connectPlayer, jump } from 'Player';
import { useModal } from 'App/components/Modal';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import { DATADOG, SENTRY, STACKDRIVER, typeList } from 'Types/session/stackEvent';
import { connect } from 'react-redux';
import { typeList } from 'Types/session/stackEvent';
import StackEventRow from 'Shared/DevTools/StackEventRow';
import StackEventModal from '../StackEventModal';
let timeOut: any = null;
const TIMEOUT_DURATION = 5000;
import StackEventModal from '../StackEventModal';
import useAutoscroll from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'
import useCellMeasurerCache from '../useCellMeasurerCache'
const INDEX_KEY = 'stackEvent';
const ALL = 'ALL';
const TABS = [ALL, ...typeList].map((tab) => ({ text: tab, key: tab }));
const TAB_KEYS = [ ALL, ...typeList] as const
const TABS = TAB_KEYS.map((tab) => ({ text: tab, key: tab }))
function StackEventPanel() {
const { player, store } = React.useContext(PlayerContext)
const jump = (t: number) => player.jump(t)
const { stackList: list, stackListNow: listNow } = store.get()
interface Props {
list: any;
hideHint: any;
time: any;
}
function StackEventPanel(props: Props) {
const { list, time } = props;
const additionalHeight = 0;
const {
sessionStore: { devTools },
} = useStore();
const { showModal, component: modalActive } = useModal();
const [filteredList, setFilteredList] = useState([]);
const filter = useObserver(() => devTools[INDEX_KEY].filter);
const activeTab = useObserver(() => devTools[INDEX_KEY].activeTab);
const activeIndex = useObserver(() => devTools[INDEX_KEY].index);
const [pauseSync, setPauseSync] = useState(activeIndex > 0);
const synRef: any = useRef({});
synRef.current = {
pauseSync,
const { showModal } = useModal();
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false) // TODO:embed that into useModal
const filter = devTools[INDEX_KEY].filter
const activeTab = devTools[INDEX_KEY].activeTab
const activeIndex = devTools[INDEX_KEY].index
let filteredList = useRegExListFilterMemo(list, it => it.name, filter)
filteredList = useTabListFilterMemo(filteredList, it => it.source, ALL, activeTab)
const onTabClick = (activeTab: typeof TAB_KEYS[number]) => devTools.update(INDEX_KEY, { activeTab })
const onFilterChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => devTools.update(INDEX_KEY, { filter: value })
const tabs = useMemo(() =>
TABS.filter(({ key }) => key === ALL || list.some(({ source }) => key === source)),
[ list.length ],
)
const [
timeoutStartAutoscroll,
stopAutoscroll,
] = useAutoscroll(
filteredList,
listNow[listNow.length-1].time,
activeIndex,
};
const _list = React.useRef();
const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab });
const onFilterChange = ({ target: { value } }: any) => {
devTools.update(INDEX_KEY, { filter: value });
};
const getCurrentIndex = () => {
return filteredList.filter((item: any) => item.time <= time).length - 1;
};
const removePause = () => {
if (!!modalActive) return;
clearTimeout(timeOut);
timeOut = setTimeout(() => {
devTools.update(INDEX_KEY, { index: getCurrentIndex() });
setPauseSync(false);
}, TIMEOUT_DURATION);
};
useEffect(() => {
const currentIndex = getCurrentIndex();
if (currentIndex !== activeIndex && !pauseSync) {
devTools.update(INDEX_KEY, { index: currentIndex });
}
}, [time]);
index => devTools.update(INDEX_KEY, { index })
)
const onMouseEnter = stopAutoscroll
const onMouseLeave = () => {
removePause();
};
if (isDetailsModalActive) { return }
timeoutStartAutoscroll()
}
React.useMemo(() => {
const filterRE = getRE(filter, 'i');
let list = props.list;
list = list.filter(
({ name, source }: any) =>
(!!filter ? filterRE.test(name) : true) && (activeTab === ALL || activeTab === source)
);
setFilteredList(list);
}, [filter, activeTab]);
const tabs = useMemo(() => {
return TABS.filter(({ key }) => key === ALL || list.some(({ source }: any) => key === source));
}, []);
const cache = new CellMeasurerCache({
fixedWidth: true,
keyMapper: (index: number) => filteredList[index],
});
const cache = useCellMeasurerCache(filteredList)
const showDetails = (item: any) => {
clearTimeout(timeOut);
showModal(<StackEventModal event={item} />, { right: true, onClose: removePause });
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
setPauseSync(true);
};
setIsDetailsModalActive(true)
showModal(
<StackEventModal event={item} />,
{
right: true,
onClose: () => {
setIsDetailsModalActive(false)
timeoutStartAutoscroll()
}
}
)
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) })
stopAutoscroll()
}
const _list = React.useRef()
useEffect(() => {
if (_list.current) {
// @ts-ignore
_list.current.scrollToRow(activeIndex)
}
}, [ activeIndex ])
const _rowRenderer = ({ index, key, parent, style }: any) => {
const item = filteredList[index];
@ -114,7 +98,7 @@ function StackEventPanel(props: Props) {
key={item.key}
event={item}
onJump={() => {
setPauseSync(true);
stopAutoscroll()
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
jump(item.time);
}}
@ -123,19 +107,12 @@ function StackEventPanel(props: Props) {
)}
</CellMeasurer>
);
};
useEffect(() => {
if (_list.current) {
// @ts-ignore
_list.current.scrollToRow(activeIndex);
}
}, [activeIndex]);
}
return (
<BottomBlock
style={{ height: 300 + additionalHeight + 'px' }}
onMouseEnter={() => setPauseSync(true)}
style={{ height: '300px' }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<BottomBlock.Header>
@ -147,7 +124,6 @@ function StackEventPanel(props: Props) {
className="input-small h-8"
placeholder="Filter by keyword"
icon="search"
iconPosition="left"
name="filter"
height={28}
onChange={onFilterChange}
@ -186,16 +162,4 @@ function StackEventPanel(props: Props) {
);
}
export default connect(
(state: any) => ({
hintIsHidden:
state.getIn(['components', 'player', 'hiddenHints', 'stack']) ||
!state.getIn(['site', 'list']).some((s: any) => s.stackIntegrations),
}),
{ hideHint }
)(
connectPlayer((state: any) => ({
list: state.stackList,
time: state.time,
}))(StackEventPanel)
);
export default observer(StackEventPanel)

View file

@ -0,0 +1,61 @@
import { useEffect, useState, useRef, useMemo } from 'react'
import { Timed } from 'Player'
import useLatestRef from 'App/hooks/useLatestRef'
import useCancelableTimeout from 'App/hooks/useCancelableTimeout'
const TIMEOUT_DURATION = 5000;
function useAutoupdate<T>(
savedValue: T,
actualValue: T,
resetValue: T,
updadteValue: (value: T) => void,
) {
const [ autoupdate, setAutoupdate ] = useState(savedValue === resetValue)
const [ timeoutStartAutoupdate, stopAutoupdate ] = useCancelableTimeout(
() => setAutoupdate(true),
() => setAutoupdate(false),
TIMEOUT_DURATION,
)
useEffect(() => {
if (autoupdate && actualValue !== savedValue) {
updadteValue(actualValue)
}
}, [ autoupdate, actualValue ])
const autoScrollRef = useLatestRef(autoupdate)
useEffect(() => {
if (!autoupdate) {
timeoutStartAutoupdate()
}
return () => {
if (autoScrollRef.current) {
updadteValue(resetValue)
}
}
}, [])
return [ timeoutStartAutoupdate, stopAutoupdate ]
}
// That might be simplified by removing index from devTools[INDEX_KEY] store...
export default function useAutoscroll(
filteredList: Timed[],
time: number,
savedIndex: number,
updadteIndex: (index: number) => void,
) {
const filteredIndexNow = useMemo(() => {
// Should use findLastIndex here
for (let i=0; i < filteredList.length; i++) {
if (filteredList[i].time > time) {
return i-1
}
}
return filteredList.length
}, [ time, filteredList ])
return useAutoupdate(savedIndex, filteredIndexNow, 0, updadteIndex)
}

View file

@ -0,0 +1,12 @@
import { useMemo } from 'react'
import { CellMeasurerCache } from 'react-virtualized';
import useLatestRef from 'App/hooks/useLatestRef'
export default function useCellMeasurerCache(itemList: any[]) {
const filteredListRef = useLatestRef(itemList)
return useMemo(() => new CellMeasurerCache({
fixedWidth: true,
keyMapper: (index) => filteredListRef.current[index],
}), [])
}

View file

@ -0,0 +1,34 @@
import { useMemo } from 'react'
import { getRE } from 'App/utils'
// TODO: merge with utils/filterList (use logic of string getter like here instead of using callback)
export function useRegExListFilterMemo<T>(
list: T[],
filterBy: (it: T) => string | string[],
reText: string,
) {
return useMemo(() => {
if (!reText) { return list }
const re = getRE(reText, 'i')
return list.filter(it => {
const strs = filterBy(it)
return Array.isArray(strs)
? strs.some(s => re.test(s))
: re.test(strs)
})
}, [ list, list.length, reText ])
}
export function useTabListFilterMemo<T, Tab=string>(
list: T[],
itemToTab: (it: T) => Tab,
commonTab: Tab,
currentTab: Tab,
) {
return useMemo(() => {
if (currentTab === commonTab) { return list }
return list.filter(it => itemToTab(it) === currentTab)
}, [ list, list.length, currentTab ])
}

View file

@ -16,7 +16,7 @@ function FetchDetailsModal(props: Props) {
const [resource, setResource] = useState(props.resource);
const [first, setFirst] = useState(false);
const [last, setLast] = useState(false);
const isXHR = resource.type === TYPES.XHR || resource.type === TYPES.FETCH;
const isXHR = resource.type === TYPES.XHR
const {
sessionStore: { devTools },
} = useStore();

View file

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Controls as Player } from 'Player';
import { Tooltip } from 'UI';
import { INDEXES } from 'App/constants/zindex';
import { PlayerContext } from 'App/components/Session/playerContext';
export const FEATURE_KEYS = {
XRAY: 'featureViewed',
@ -16,6 +16,8 @@ interface IProps {
}
export default function GuidePopup({ children, title, description }: IProps) {
const { player: Player } = React.useContext(PlayerContext)
const [showGuide, setShowGuide] = useState(!localStorage.getItem(FEATURE_KEYS.NOTES));
useEffect(() => {
if (!showGuide) {

View file

@ -35,7 +35,6 @@ function LiveSessionSearchField(props: Props) {
onBlur={ () => setTimeout(setShowModal, 200, false) }
onChange={ onSearchChange }
icon="search"
iconPosition="left"
placeholder={ 'Find live sessions by user or metadata.'}
fluid
id="search"
@ -56,4 +55,4 @@ function LiveSessionSearchField(props: Props) {
);
}
export default connect(null, { fetchFilterSearch, editFilter, addFilterByKeyAndValue })(LiveSessionSearchField);
export default connect(null, { fetchFilterSearch, editFilter, addFilterByKeyAndValue })(LiveSessionSearchField);

View file

@ -1,15 +1,14 @@
import React from 'react';
import { Button, Icon } from 'UI';
import copy from 'copy-to-clipboard';
import { connectPlayer } from 'Player';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
interface Props {
content: string;
time: any;
}
function SessionCopyLink({ content = '', time }: Props) {
function SessionCopyLink() {
const [copied, setCopied] = React.useState(false);
const { store } = React.useContext(PlayerContext)
const time = store.get().time
const copyHandler = () => {
setCopied(true);
@ -21,12 +20,6 @@ function SessionCopyLink({ content = '', time }: Props) {
return (
<div className="flex justify-between items-center w-full mt-2">
{/* <IconButton
label="Copy URL at current time"
primaryText
icon="link-45deg"
onClick={copyHandler}
/> */}
<Button variant="text-primary" onClick={copyHandler}>
<>
<Icon name="link-45deg" className="mr-2" color="teal" size="18" />
@ -38,8 +31,4 @@ function SessionCopyLink({ content = '', time }: Props) {
);
}
const SessionCopyLinkCompo = connectPlayer((state: any) => ({
time: state.time,
}))(SessionCopyLink);
export default React.memo(SessionCopyLinkCompo);
export default observer(SessionCopyLink);

View file

@ -2,15 +2,16 @@ import React, { useEffect, useState } from 'react';
import stl from './xrayButton.module.css';
import cn from 'classnames';
import { Tooltip } from 'UI';
import GuidePopup, { FEATURE_KEYS } from 'Shared/GuidePopup';
import { Controls as Player } from 'Player';
import { INDEXES } from 'App/constants/zindex';
import { FEATURE_KEYS } from 'Shared/GuidePopup';
import { PlayerContext } from 'App/components/Session/playerContext';
interface Props {
onClick?: () => void;
isActive?: boolean;
}
function XRayButton(props: Props) {
const { player: Player } = React.useContext(PlayerContext);
const { isActive } = props;
// const [showGuide, setShowGuide] = useState(!localStorage.getItem(FEATURE_KEYS.XRAY));
const showGuide = false;
@ -41,40 +42,14 @@ function XRayButton(props: Props) {
></div>
)}
<div className="relative">
{showGuide ? (
// <GuidePopup
// title={<div className="color-gray-dark">Introducing <span className={stl.text}>X-Ray</span></div>}
// description={"Get a quick overview on the issues in this session."}
// >
<button
className={cn(stl.wrapper, { [stl.default]: !isActive, [stl.active]: isActive })}
onClick={onClick}
style={{ zIndex: INDEXES.POPUP_GUIDE_BTN, position: 'relative' }}
>
<span className="z-1">X-RAY</span>
</button>
// <div
// className="absolute bg-white top-0 left-0 z-0"
// style={{
// zIndex: INDEXES.POPUP_GUIDE_BG,
// width: '100px',
// height: '50px',
// borderRadius: '30px',
// margin: '-10px -16px',
// }}
// ></div>
// </GuidePopup>
) : (
<Tooltip title="Get a quick overview on the issues in this session." disabled={isActive}>
<button
className={cn(stl.wrapper, { [stl.default]: !isActive, [stl.active]: isActive })}
onClick={onClick}
>
<span className="z-1">X-RAY</span>
</button>
</Tooltip>
)}
<Tooltip title="Get a quick overview on the issues in this session." disabled={isActive}>
<button
className={cn(stl.wrapper, { [stl.default]: !isActive, [stl.active]: isActive })}
onClick={onClick}
>
<span className="z-1">X-RAY</span>
</button>
</Tooltip>
</div>
</>
);

View file

@ -5,7 +5,7 @@ import { CircularLoader, Icon, Tooltip } from 'UI';
interface Props {
className?: string;
children?: React.ReactNode;
onClick?: () => void;
onClick?: (e: React.MouseEvent<HTMLElement>) => void;
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
variant?: 'default' | 'primary' | 'text' | 'text-primary' | 'text-red' | 'outline' | 'green';

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

@ -0,0 +1,20 @@
import { useRef, useEffect } from 'react'
export default function useCancelableTimeout(
onTimeout: ()=> void,
onCancel: ()=> void,
delay: number,
): [()=> void, ()=> void] {
const idRef = useRef<ReturnType<typeof setTimeout>>()
const triggerTimeout = () => {
clearTimeout(idRef.current)
idRef.current = setTimeout(onTimeout, delay)
}
const cancelTimeout = () => {
clearTimeout(idRef.current)
onCancel()
}
useEffect(() => () => clearTimeout(idRef.current)) // auto-cancel without callback (clean)
return [ triggerTimeout, cancelTimeout ]
}

View file

@ -0,0 +1,8 @@
import { useRef, useEffect } from 'react'
export default function useLatestRef<T>(state: T) {
const ref = useRef<T>(state)
useEffect(() => { ref.current = state }, [ state ])
return ref
}

View file

@ -2,13 +2,9 @@ import { makeAutoObservable } from "mobx"
import { notesService } from "App/services"
import { Note, WriteNote, iTag, NotesFilter } from 'App/services/NotesService'
interface SessionNotes {
[sessionId: string]: Note[]
}
export default class NotesStore {
notes: Note[] = []
sessionNotes: SessionNotes = {}
sessionNotes: Note[] = []
loading: boolean
page = 1
pageSize = 10
@ -48,7 +44,7 @@ export default class NotesStore {
this.loading = true
try {
const notes = await notesService.getNotesBySessionId(sessionId)
this.sessionNotes[sessionId] = notes
this.sessionNotes = notes
return notes;
} catch (e) {
console.error(e)

View file

@ -1,84 +0,0 @@
import Marker from './Marker';
import Cursor from './Cursor';
import Inspector from './Inspector';
// import styles from './screen.module.css';
// import { getState } from '../../../store';
import BaseScreen from './BaseScreen';
export { INITIAL_STATE } from './BaseScreen';
export type { State } from './BaseScreen';
export default class Screen extends BaseScreen {
public readonly cursor: Cursor;
private substitutor: BaseScreen | null = null;
private inspector: Inspector | null = null;
private marker: Marker | null = null;
constructor() {
super();
this.cursor = new Cursor(this.overlay);
}
getCursorTarget() {
return this.getElementFromInternalPoint(this.cursor.getPosition());
}
getCursorTargets() {
return this.getElementsFromInternalPoint(this.cursor.getPosition());
}
_scale() {
super._scale();
if (this.substitutor) {
this.substitutor._scale();
}
}
enableInspector(clickCallback: ({ target: Element }) => void): Document | null {
if (!this.parentElement) return null;
if (!this.substitutor) {
this.substitutor = new Screen();
this.marker = new Marker(this.substitutor.overlay, this.substitutor);
this.inspector = new Inspector(this.substitutor, this.marker);
//this.inspector.addClickListener(clickCallback, true);
this.substitutor.attach(this.parentElement);
}
this.substitutor.display(false);
const docElement = this.document?.documentElement; // this.substitutor.document?.importNode(
const doc = this.substitutor.document;
if (doc && docElement) {
// doc.documentElement.innerHTML = "";
// // Better way?
// for (let i = 1; i < docElement.attributes.length; i++) {
// const att = docElement.attributes[i];
// doc.documentElement.setAttribute(att.name, att.value);
// }
// for (let i = 1; i < docElement.childNodes.length; i++) {
// doc.documentElement.appendChild(docElement.childNodes[i].cloneNode(true));
// }
doc.open();
doc.write(docElement.outerHTML); // Context will be iframe, so instanceof Element won't work
doc.close();
// TODO! : copy stylesheets, check with styles
}
this.display(false);
this.inspector.toggle(true, clickCallback);
this.substitutor.display(true);
return doc;
}
disableInspector() {
if (this.substitutor) {
const doc = this.substitutor.document;
if (doc) {
doc.documentElement.innerHTML = "";
}
this.inspector.toggle(false);
this.substitutor.display(false);
}
this.display(true);
}
}

View file

@ -1,2 +0,0 @@
export { default } from './Screen';
export * from './Screen';

View file

@ -1,4 +0,0 @@
export interface Point {
x: number;
y: number;
}

View file

@ -1,152 +0,0 @@
import Screen, { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen/Screen';
import { update, getState } from '../../store';
import type { Point } from './Screen/types';
function getOffset(el: Element, innerWindow: Window) {
const rect = el.getBoundingClientRect();
return {
fixedLeft: rect.left + innerWindow.scrollX,
fixedTop: rect.top + innerWindow.scrollY,
rect,
};
}
//export interface targetPosition
interface BoundingRect {
top: number,
left: number,
width: number,
height: number,
}
export interface MarkedTarget {
boundingRect: BoundingRect,
el: Element,
selector: string,
count: number,
index: number,
active?: boolean,
percent: number
}
export interface State extends SuperState {
messagesLoading: boolean,
cssLoading: boolean,
markedTargets: MarkedTarget[] | null,
activeTargetIndex: number,
}
export const INITIAL_STATE: State = {
...SUPER_INITIAL_STATE,
messagesLoading: false,
cssLoading: false,
markedTargets: null,
activeTargetIndex: 0
};
export default class StatedScreen extends Screen {
constructor() { super(); }
setMessagesLoading(messagesLoading: boolean) {
this.display(!messagesLoading);
update({ messagesLoading });
}
setCSSLoading(cssLoading: boolean) {
this.displayFrame(!cssLoading);
update({ cssLoading });
}
setSize({ height, width }: { height: number, width: number }) {
update({ width, height });
this.scale();
this.updateMarketTargets()
}
updateMarketTargets() {
const { markedTargets } = getState();
if (markedTargets) {
update({
markedTargets: markedTargets.map((mt: any) => ({
...mt,
boundingRect: this.calculateRelativeBoundingRect(mt.el),
})),
});
}
}
private calculateRelativeBoundingRect(el: Element): BoundingRect {
if (!this.parentElement) return {top:0, left:0, width:0,height:0} //TODO
const { top, left, width, height } = el.getBoundingClientRect();
const s = this.getScale();
const scrinRect = this.screen.getBoundingClientRect();
const parentRect = this.parentElement.getBoundingClientRect();
return {
top: top*s + scrinRect.top - parentRect.top,
left: left*s + scrinRect.left - parentRect.left,
width: width*s,
height: height*s,
}
}
setActiveTarget(index: number) {
const window = this.window
const markedTargets: MarkedTarget[] | null = getState().markedTargets
const target = markedTargets && markedTargets[index]
if (target && window) {
const { fixedTop, rect } = getOffset(target.el, window)
const scrollToY = fixedTop - window.innerHeight / 1.5
if (rect.top < 0 || rect.top > window.innerHeight) {
// behavior hack TODO: fix it somehow when they will decide to remove it from browser api
// @ts-ignore
window.scrollTo({ top: scrollToY, behavior: 'instant' })
setTimeout(() => {
if (!markedTargets) { return }
update({
markedTargets: markedTargets.map(t => t === target ? {
...target,
boundingRect: this.calculateRelativeBoundingRect(target.el),
} : t)
})
}, 0)
}
}
update({ activeTargetIndex: index });
}
private actualScroll: Point | null = null
setMarkedTargets(selections: { selector: string, count: number }[] | null) {
if (selections) {
const totalCount = selections.reduce((a, b) => {
return a + b.count
}, 0);
const markedTargets: MarkedTarget[] = [];
let index = 0;
selections.forEach((s) => {
const el = this.getElementBySelector(s.selector);
if (!el) return;
markedTargets.push({
...s,
el,
index: index++,
percent: Math.round((s.count * 100) / totalCount),
boundingRect: this.calculateRelativeBoundingRect(el),
count: s.count,
})
});
this.actualScroll = this.getCurrentScroll()
update({ markedTargets });
} else {
if (this.actualScroll) {
this.window?.scrollTo(this.actualScroll.x, this.actualScroll.y)
this.actualScroll = null
}
update({ markedTargets: null });
}
}
}

View file

@ -1,2 +0,0 @@
export { default } from './StatedScreen';
export * from './StatedScreen';

View file

@ -1,2 +0,0 @@
export { default } from './MessageDistributor';
export * from './MessageDistributor';

View file

@ -1,41 +0,0 @@
import type StatedScreen from '../StatedScreen';
import type { MouseMove } from '../messages';
import ListWalker from './ListWalker';
const HOVER_CLASS = "-openreplay-hover";
const HOVER_CLASS_DEPR = "-asayer-hover";
export default class MouseMoveManager extends ListWalker<MouseMove> {
private hoverElements: Array<Element> = [];
constructor(private screen: StatedScreen) {super()}
private updateHover(): void {
const curHoverElements = this.screen.getCursorTargets();
const diffAdd = curHoverElements.filter(elem => !this.hoverElements.includes(elem));
const diffRemove = this.hoverElements.filter(elem => !curHoverElements.includes(elem));
this.hoverElements = curHoverElements;
diffAdd.forEach(elem => {
elem.classList.add(HOVER_CLASS)
elem.classList.add(HOVER_CLASS_DEPR)
});
diffRemove.forEach(elem => {
elem.classList.remove(HOVER_CLASS)
elem.classList.remove(HOVER_CLASS_DEPR)
});
}
reset(): void {
this.hoverElements = [];
}
move(t: number) {
const lastMouseMove = this.moveGetLast(t);
if (!!lastMouseMove){
this.screen.cursor.move(lastMouseMove);
//window.getComputedStyle(this.screen.getCursorTarget()).cursor === 'pointer' // might nfluence performance though
this.updateHover();
}
}
}

Some files were not shown because too many files have changed in this diff Show more