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:
commit
5a4480599f
175 changed files with 4172 additions and 4344 deletions
87
.github/workflows/frontend-dev.yaml
vendored
Normal file
87
.github/workflows/frontend-dev.yaml
vendored
Normal 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 }}
|
||||
2
.github/workflows/frontend.yaml
vendored
2
.github/workflows/frontend.yaml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Frontend FOSS Deployment
|
||||
name: Frontend Dev Deployment
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
);
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
79
frontend/app/components/Session/PlayerContent.js
Normal file
79
frontend/app/components/Session/PlayerContent.js
Normal 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);
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
129
frontend/app/components/Session/WebPlayer.tsx
Normal file
129
frontend/app/components/Session/WebPlayer.tsx
Normal 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));
|
||||
12
frontend/app/components/Session/playerContext.ts
Normal file
12
frontend/app/components/Session/playerContext.ts
Normal 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);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './Autoplay'
|
||||
|
|
@ -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 = ({
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
// import { NONE, CONSOLE, NETWORK, STACKEVENTS, REDUX_STATE, PROFILER, PERFORMANCE, GRAPHQL } from 'Duck/components/player';
|
||||
//
|
||||
//
|
||||
// export default {
|
||||
// [NONE]: {
|
||||
// Component: null,
|
||||
//
|
||||
// }
|
||||
// }
|
||||
|
|
@ -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} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ConsoleRow';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
125
frontend/app/components/Session_/Exceptions/Exceptions.tsx
Normal file
125
frontend/app/components/Session_/Exceptions/Exceptions.tsx
Normal 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));
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
174
frontend/app/components/Session_/GraphQL/GraphQL.tsx
Normal file
174
frontend/app/components/Session_/GraphQL/GraphQL.tsx
Normal 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);
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
</>
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
export { default } from './LongTasks';
|
||||
export { default } from './LongTasks.DEPRECATED';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 : '';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export { default } from './Performance';
|
||||
export * from './Performance';
|
||||
export { ConnectedPerformance as default } from './Performance';
|
||||
export * from './Performance';
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const ControlButton = ({
|
|||
hasErrors = false,
|
||||
active = false,
|
||||
size = 20,
|
||||
noLabel,
|
||||
noLabel = false,
|
||||
labelClassName,
|
||||
containerClassName,
|
||||
noIcon,
|
||||
|
|
|
|||
420
frontend/app/components/Session_/Player/Controls/Controls.tsx
Normal file
420
frontend/app/components/Session_/Player/Controls/Controls.tsx
Normal 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;
|
||||
// }
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>}>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
161
frontend/app/components/Session_/PlayerBlockHeader.tsx
Normal file
161
frontend/app/components/Session_/PlayerBlockHeader.tsx
Normal 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);
|
||||
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
1
frontend/app/components/Session_/QueueControls/index.ts
Normal file
1
frontend/app/components/Session_/QueueControls/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './QueueControls'
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
308
frontend/app/components/Session_/Storage/Storage.tsx
Normal file
308
frontend/app/components/Session_/Storage/Storage.tsx
Normal 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 you’re 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));
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
// import { NONE, CONSOLE, NETWORK, STACKEVENTS, REDUX_STATE, PROFILER, PERFORMANCE, GRAPHQL } from 'Duck/components/player';
|
||||
//
|
||||
//
|
||||
// export default {
|
||||
// [NONE]: {
|
||||
// Component: null,
|
||||
//
|
||||
// }
|
||||
// }
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
61
frontend/app/components/shared/DevTools/useAutoscroll.ts
Normal file
61
frontend/app/components/shared/DevTools/useAutoscroll.ts
Normal 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)
|
||||
}
|
||||
|
|
@ -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],
|
||||
}), [])
|
||||
}
|
||||
34
frontend/app/components/shared/DevTools/useListFilter.ts
Normal file
34
frontend/app/components/shared/DevTools/useListFilter.ts
Normal 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 ])
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
20
frontend/app/hooks/useCancelableTimeout.ts
Normal file
20
frontend/app/hooks/useCancelableTimeout.ts
Normal 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 ]
|
||||
}
|
||||
8
frontend/app/hooks/useLatestRef.ts
Normal file
8
frontend/app/hooks/useLatestRef.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from './Screen';
|
||||
export * from './Screen';
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
export interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from './StatedScreen';
|
||||
export * from './StatedScreen';
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from './MessageDistributor';
|
||||
export * from './MessageDistributor';
|
||||
|
|
@ -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
Loading…
Add table
Reference in a new issue