feat(ui): add shortcuts to player page, clear unused files (#1963)
* feat(ui): remove dead code, add more shortcuts to player * feat(ui): remove dead code, add more shortcuts to player * feat(ui): add shortcuts * feat(ui): additional menu shortcuts * fix(ui): fix depds for effect
This commit is contained in:
parent
902ec87d7a
commit
c0f4a99545
58 changed files with 584 additions and 2473 deletions
|
|
@ -1,67 +0,0 @@
|
|||
import React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import ImagePlayer from 'Player/ios/ImagePlayer';
|
||||
import { CRASHES, NETWORK, LOGS, PERFORMANCE, CUSTOM } from 'Player/ios/state';
|
||||
import Network from './IOSPlayer/Network';
|
||||
import Logs from './IOSPlayer/Logs';
|
||||
import IMGScreen from './IOSPlayer/IMGScreen';
|
||||
import Crashes from './IOSPlayer/Crashes';
|
||||
import Performance from './IOSPlayer/Performance';
|
||||
import StackEvents from './IOSPlayer/StackEvents';
|
||||
import Layout from './Layout/Layout';
|
||||
|
||||
const TOOLBAR = [
|
||||
{
|
||||
Component: Network,
|
||||
key: NETWORK,
|
||||
label: "Network",
|
||||
icon: "wifi",
|
||||
},
|
||||
{
|
||||
Component: Logs,
|
||||
key: LOGS,
|
||||
label: "Logs",
|
||||
icon: "console",
|
||||
},
|
||||
{
|
||||
Component: Crashes,
|
||||
key: CRASHES,
|
||||
label: "Crashes",
|
||||
icon: "console/error",
|
||||
},
|
||||
{
|
||||
Component: StackEvents,
|
||||
key: CUSTOM,
|
||||
label: "Events",
|
||||
icon: "puzzle-piece",
|
||||
},
|
||||
{
|
||||
Component: Performance,
|
||||
key: PERFORMANCE,
|
||||
label: "Performance",
|
||||
icon: "tachometer-slow",
|
||||
showCount: false,
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
export default function IOSPlayer({ session }) {
|
||||
const [player] = useState(() => new ImagePlayer(session));
|
||||
useEffect(() => {
|
||||
player.attach({ wrapperId: "IOSWrapper", screenId: "IOSscreen" });
|
||||
return () => {
|
||||
player.clean();
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Layout player={ player } toolbar={TOOLBAR} >
|
||||
<IMGScreen
|
||||
screenId="IOSscreen"
|
||||
wrapperId="IOSWrapper"
|
||||
device={ session.userDevice }
|
||||
player={ player }
|
||||
/>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Input, NoContent } from 'UI';
|
||||
import { getRE } from 'App/utils';
|
||||
import { CRASHES } from 'Player/ios/state';
|
||||
|
||||
import * as PanelLayout from '../Layout/ToolPanel/PanelLayout';
|
||||
|
||||
import Autoscroll from 'Components/Session_/Autoscroll';
|
||||
|
||||
|
||||
function Crashes({ player }) {
|
||||
|
||||
const [ filter, setFilter ] = useState("");
|
||||
const filterRE = getRE(filter, 'i');
|
||||
const filtered = player.lists[CRASHES].listNow.filter(({ name, reason, stacktrace }) =>
|
||||
filterRE.test(name) || filterRE.test(reason) || filterRE.test(stacktrace)
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<PanelLayout.Header>
|
||||
<Input
|
||||
className="input-small"
|
||||
placeholder="Filter"
|
||||
icon="search"
|
||||
name="filter"
|
||||
onChange={ setFilter }
|
||||
/>
|
||||
</PanelLayout.Header>
|
||||
<PanelLayout.Body>
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ filtered.length === 0}
|
||||
>
|
||||
<Autoscroll>
|
||||
{ filtered.map(c => (
|
||||
<div className="border-b border-gray-light-shade error p-2">
|
||||
<h4>{ c.name }</h4>
|
||||
<h5><i>{`Reason: "${c.reason}"`}</i></h5>
|
||||
<div className="whitespace-pre pl-5">{ c.stacktrace }</div>
|
||||
</div>
|
||||
))}
|
||||
</Autoscroll>
|
||||
</NoContent>
|
||||
</PanelLayout.Body>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Crashes);
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import cn from 'classnames';
|
||||
import Screen from './ScreenWithLoaders';
|
||||
//import cls from './HTMLScreen.module.css';
|
||||
|
||||
|
||||
const DEVICE_MAP = {
|
||||
"iPhone 4s": "iphone4s",
|
||||
"iPhone 5s": "iphone5s",
|
||||
"iPhone 5c": "iphone5c",
|
||||
"iPhone 8": "iphone8",
|
||||
"iPhone 8 Plus": "iphone8plus",
|
||||
"iPhone X": "iphone-x",
|
||||
}
|
||||
|
||||
function mapDevice(device) {
|
||||
if (!!DEVICE_MAP[ device ]) {
|
||||
return DEVICE_MAP[ device ];
|
||||
}
|
||||
if (device.includes("iPhone")) {
|
||||
return "iphone8"; // ??
|
||||
}
|
||||
if (device.includes("iPad")) {
|
||||
return "iPad";
|
||||
}
|
||||
return "macbook";
|
||||
}
|
||||
|
||||
function getDefaultColor(type) {
|
||||
if (type === "iphone5c") {
|
||||
return "white";
|
||||
}
|
||||
if (type === "iphone-x" || type === "macbook") {
|
||||
return "";
|
||||
}
|
||||
return "black";
|
||||
}
|
||||
|
||||
|
||||
function BeforeScreen({ type }) {
|
||||
if (type === "ipad") {
|
||||
return <div className="camera"></div>
|
||||
}
|
||||
if (type === "iphone-x") {
|
||||
return (
|
||||
<>
|
||||
<div className="notch">
|
||||
<div className="camera"></div>
|
||||
<div className="speaker"></div>
|
||||
</div>
|
||||
<div className="top-bar"></div>
|
||||
<div className="sleep"></div>
|
||||
<div className="bottom-bar"></div>
|
||||
<div className="volume"></div>
|
||||
<div className="overflow">
|
||||
<div className="shadow shadow--tr"></div>
|
||||
<div className="shadow shadow--tl"></div>
|
||||
<div className="shadow shadow--br"></div>
|
||||
<div className="shadow shadow--bl"></div>
|
||||
</div>
|
||||
<div className="inner-shadow"></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (type === "macbook") {
|
||||
return (
|
||||
<>
|
||||
<div className="top-bar"></div>
|
||||
<div className="camera"></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="top-bar"></div>
|
||||
<div className="sleep"></div>
|
||||
<div className="volume"></div>
|
||||
<div className="camera"></div>
|
||||
<div className="sensor"></div>
|
||||
<div className="speaker"></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AfterScreen({ type }) {
|
||||
if (type === "ipad") {
|
||||
return <div className="home"></div>;
|
||||
}
|
||||
if (type === "macbook") {
|
||||
return <div className="bottom-bar"></div>;
|
||||
}
|
||||
if (type === "iphone-x") {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="home"></div>
|
||||
<div className="bottom-bar"></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function HTMLScreen({ wrapperId, screenId, device, player }) {
|
||||
const type = mapDevice(device);
|
||||
const color = getDefaultColor(type);
|
||||
|
||||
return (
|
||||
<>
|
||||
<link rel="stylesheet" type="text/css" href="/marvel-device.css" />
|
||||
<div
|
||||
className={ cn("marvel-device iphone5c", type, color, { "landscape": player.state.orientationLandscape }) }
|
||||
id={wrapperId}
|
||||
>
|
||||
<BeforeScreen type={ type } />
|
||||
<Screen className="screen" screenId={screenId} player={player}/>
|
||||
<AfterScreen type={ type } />
|
||||
</div>
|
||||
{/* <div className={cls.iphone} id={ id }> */}
|
||||
{/* <i></i> */}
|
||||
{/* <b></b> */}
|
||||
{/* <span></span> */}
|
||||
{/* <span></span> */}
|
||||
{/* </div> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default observer(HTMLScreen);
|
||||
|
||||
// iphone8 iphone8plus iphone5s iphone5c(white) iphone4s
|
||||
// <div className="marvel-device iphone8 black">
|
||||
// <div className="top-bar"></div>
|
||||
// <div className="sleep"></div>
|
||||
// <div className="volume"></div>
|
||||
// <div className="camera"></div>
|
||||
// <div className="sensor"></div>
|
||||
// <div className="speaker"></div>
|
||||
// <div className="screen">
|
||||
// <!-- Content goes here -->
|
||||
// </div>
|
||||
// <div className="home"></div>
|
||||
// <div className="bottom-bar"></div>
|
||||
// </div>
|
||||
//
|
||||
//
|
||||
// <div className="marvel-device iphone-x">
|
||||
// <div className="notch">
|
||||
// <div className="camera"></div>
|
||||
// <div className="speaker"></div>
|
||||
// </div>
|
||||
// <div className="top-bar"></div>
|
||||
// <div className="sleep"></div>
|
||||
// <div className="bottom-bar"></div>
|
||||
// <div className="volume"></div>
|
||||
// <div className="overflow">
|
||||
// <div className="shadow shadow--tr"></div>
|
||||
// <div className="shadow shadow--tl"></div>
|
||||
// <div className="shadow shadow--br"></div>
|
||||
// <div className="shadow shadow--bl"></div>
|
||||
// </div>
|
||||
// <div className="inner-shadow"></div>
|
||||
// <div className="screen">
|
||||
// <!-- Content goes here -->
|
||||
// </div>
|
||||
// </div>
|
||||
//
|
||||
// <div className="marvel-device ipad silver">
|
||||
// <div className="camera"></div>
|
||||
// <div className="screen">
|
||||
// <!-- Content goes here -->
|
||||
// </div>
|
||||
// <div className="home"></div>
|
||||
// </div>
|
||||
//
|
||||
//
|
||||
// <div className="marvel-device macbook">
|
||||
// <div className="top-bar"></div>
|
||||
// <div className="camera"></div>
|
||||
// <div className="screen">
|
||||
// <!-- Content goes here -->
|
||||
// </div>
|
||||
// <div className="bottom-bar"></div>
|
||||
// </div>
|
||||
//
|
||||
//
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
|
||||
.iphone {
|
||||
position: absolute;
|
||||
/*margin: 10px auto;*/
|
||||
width: 360px;
|
||||
/*height: calc(100% - 20px);*/
|
||||
height: 780px;
|
||||
/*background-color: #7371ee;*/
|
||||
background-image: linear-gradient(60deg, #7371ee 1%,#a1d9d6 100%);
|
||||
border-radius: 40px;
|
||||
box-shadow: 0px 0px 0px 11px #1f1f1f, 0px 0px 0px 13px #191919, 0px 0px 0px 20px #111;
|
||||
|
||||
&:before,
|
||||
&:after{
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&:after {
|
||||
bottom: 7px;
|
||||
width: 140px;
|
||||
height: 4px;
|
||||
background-color: #f2f2f2;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
top: 0px;
|
||||
width: 56%;
|
||||
height: 30px;
|
||||
background-color: #1f1f1f;
|
||||
border-radius: 0px 0px 40px 40px;
|
||||
}
|
||||
|
||||
i,
|
||||
b,
|
||||
span {
|
||||
position: absolute;
|
||||
display: block;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
i {
|
||||
top: 0px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 6px);
|
||||
height: 8px;
|
||||
width: 15%;
|
||||
background-color: #101010;
|
||||
border-radius: 8px;
|
||||
box-shadow: inset 0px -3px 3px 0px rgba(256, 256, 256, 0.2);
|
||||
}
|
||||
|
||||
b {
|
||||
left: 10%;
|
||||
top: 0px;
|
||||
transform: translate(180px, 4px);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: #101010;
|
||||
border-radius: 12px;
|
||||
box-shadow: inset 0px -3px 2px 0px rgba(256, 256, 256, 0.2);
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: #2d4d76;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0px -2px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
bottom: 50px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 50%;
|
||||
left: 30px;
|
||||
|
||||
& + span {
|
||||
left: auto;
|
||||
right: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import Screen from './ScreenWithLoaders';
|
||||
import HTMLScreen from './HTMLScreen';
|
||||
|
||||
const MODEL_IMG_MAP = {
|
||||
"iPhone 12" : {
|
||||
img: "iPhone-12", // 723 × 1396
|
||||
screenY: 57,
|
||||
screenX: 60,
|
||||
//width: 723,
|
||||
screenWidth: 588,
|
||||
foreground: true,
|
||||
},
|
||||
"iPhone 11" : {
|
||||
img: "iPhone-11",
|
||||
screenY: 47,
|
||||
screenX: 45,
|
||||
screenWidth: 420,
|
||||
foreground: true,
|
||||
},
|
||||
"iPhone X" : {
|
||||
img: "iPhone-X",
|
||||
screenY: 41,
|
||||
screenX: 42,
|
||||
screenWidth: 380,
|
||||
foreground: true,
|
||||
},
|
||||
"iPhone 8" : {
|
||||
img: "iPhone-8",
|
||||
screenY: 113,
|
||||
screenX: 27,
|
||||
screenWidth: 382,
|
||||
},
|
||||
"iPhone 7" : {
|
||||
img: "iPhone-7", // 476 × 923
|
||||
screenY: 117,
|
||||
screenX: 43,
|
||||
screenWidth: 380,
|
||||
},
|
||||
"iPhone 6" : {
|
||||
img: "iPhone-6", // 540 × 980
|
||||
screenY: 149,
|
||||
screenX: 81,
|
||||
screenWidth: 376,
|
||||
},
|
||||
"iPad (7th generation)" : {
|
||||
img: "iPad-7th", // 965 × 1347
|
||||
screenY: 122,
|
||||
screenX: 66,
|
||||
screenWidth: 812,
|
||||
foreground: true,
|
||||
},
|
||||
"iPad Pro (12.9-inch)" : {
|
||||
img: "iPad-pro-12.9-2020", // 1194 × 1536
|
||||
screenY: 78,
|
||||
screenX: 78,
|
||||
screenWidth: 1025,
|
||||
foreground: true,
|
||||
},
|
||||
"iPad Pro (11-inch)" : {
|
||||
img: "iPad-pro-11-2020", // 996 × 1353
|
||||
screenY: 73,
|
||||
screenX: 72,
|
||||
screenWidth: 836,
|
||||
foreground: true,
|
||||
},
|
||||
"iPad Air" : {
|
||||
img: "iPad-Air",
|
||||
screenY: 162,
|
||||
screenX: 123,
|
||||
screenWidth: 768,
|
||||
},
|
||||
"iPad Air 2": {
|
||||
img: "iPad-Air-2",
|
||||
screenY: 165,
|
||||
screenX: 118,
|
||||
screenWidth: 776,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
function getImgInfo(type) {
|
||||
let imgInfo;
|
||||
Object.keys(MODEL_IMG_MAP).map(key => {
|
||||
if (type.startsWith(key)) {
|
||||
imgInfo = MODEL_IMG_MAP[key];
|
||||
}
|
||||
});
|
||||
return imgInfo;
|
||||
}
|
||||
|
||||
|
||||
function IMGScreen(props) {
|
||||
const imgInfo = getImgInfo(props.device);
|
||||
if (!imgInfo) {
|
||||
return <HTMLScreen {...props } />;
|
||||
}
|
||||
const { wrapperId, player, screenId } = props;
|
||||
|
||||
const curScreenWidth = player.state.orientationLandscape ? player.state.height : player.state.width;
|
||||
return (
|
||||
<div id={wrapperId} className="relative">
|
||||
<Screen className="absolute inset-0" screenId={ screenId } player={player} />
|
||||
<img
|
||||
className="absolute"
|
||||
style={{
|
||||
maxWidth: 'none',
|
||||
zIndex: imgInfo.foreground ? 0 : -1,
|
||||
transformOrigin: `${imgInfo.screenX}px ${imgInfo.screenY}px`,
|
||||
transform: `
|
||||
translate(-${imgInfo.screenX}px, -${imgInfo.screenY}px)
|
||||
scale(${ curScreenWidth/imgInfo.screenWidth })
|
||||
${player.state.orientationLandscape ? console.log('wtdf') || `rotate(-90deg) translateX(-${imgInfo.screenWidth}px)` : ''}
|
||||
`,
|
||||
}}
|
||||
src={`/assets/img/ios/${imgInfo.img}.png`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(IMGScreen);
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { LOGS } from 'Player/ios/state';
|
||||
import { getRE } from 'App/utils';
|
||||
import { Tabs, Input, NoContent } from 'UI';
|
||||
import * as PanelLayout from '../Layout/ToolPanel/PanelLayout';
|
||||
import Log from '../Layout/ToolPanel/Log';
|
||||
|
||||
import Autoscroll from 'Components/Session_/Autoscroll';
|
||||
|
||||
|
||||
const ALL = 'ALL';
|
||||
const INFO = 'info';
|
||||
const ERROR = 'error';
|
||||
|
||||
const TABS = [ ALL, INFO, ERROR ].map(tab => ({ text: tab.toUpperCase(), key: tab }));
|
||||
|
||||
function Logs({ player }) {
|
||||
const [ filter, setFilter ] = useState("");
|
||||
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 }) =>
|
||||
(activeTab === ALL || activeTab === severity) && filterRE.test(content)
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<PanelLayout.Header>
|
||||
<Tabs
|
||||
tabs={ TABS }
|
||||
active={ activeTab }
|
||||
onClick={ setTab }
|
||||
border={ false }
|
||||
/>
|
||||
<Input
|
||||
className="input-small"
|
||||
placeholder="Filter"
|
||||
icon="search"
|
||||
name="filter"
|
||||
onChange={ onInputChange }
|
||||
/>
|
||||
</PanelLayout.Header>
|
||||
<PanelLayout.Body>
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ filtered.length === 0 }
|
||||
title="No recordings found"
|
||||
>
|
||||
<Autoscroll>
|
||||
{ filtered.map(log =>
|
||||
<Log text={log.content} level={log.severity}/>
|
||||
)}
|
||||
</Autoscroll>
|
||||
</NoContent>
|
||||
</PanelLayout.Body>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Logs);
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Tooltip, SlideModal } from 'UI';
|
||||
|
||||
import { NETWORK } from 'Player/ios/state';
|
||||
|
||||
import cls from './Network.module.css';
|
||||
|
||||
import TimeTable from 'Components/Session_/TimeTable';
|
||||
import FetchDetails from 'Components/Session_/Fetch/FetchDetails';
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
label: 'Status',
|
||||
dataKey: 'status',
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
label: 'Method',
|
||||
dataKey: 'method',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
label: 'url',
|
||||
width: 130,
|
||||
render: (r) => (
|
||||
<Tooltip
|
||||
title={<div className={cls.popupNameContent}>{r.url}</div>}
|
||||
size="mini"
|
||||
position="right center"
|
||||
>
|
||||
<div className={cls.popupNameTrigger}>{r.url}</div>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Size',
|
||||
width: 60,
|
||||
render: (r) => `${r.body.length}`,
|
||||
},
|
||||
{
|
||||
label: 'Time',
|
||||
width: 80,
|
||||
render: (r) => `${r.duration}ms`,
|
||||
},
|
||||
];
|
||||
|
||||
function Network({ player }) {
|
||||
const [current, setCurrent] = useState(null);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const onRowClick = useCallback((raw, index) => {
|
||||
setCurrent(raw);
|
||||
setCurrentIndex(index);
|
||||
});
|
||||
const onNextClick = useCallback(() => {
|
||||
onRowClick(player.lists[NETWORK].list[currentIndex + 1], currentIndex + 1);
|
||||
});
|
||||
const onPrevClick = useCallback(() => {
|
||||
onRowClick(player.lists[NETWORK].list[currentIndex - 1], currentIndex - 1);
|
||||
});
|
||||
const closeModal = useCallback(() => setCurrent(null)); // TODO: handle in modal
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlideModal
|
||||
size="middle"
|
||||
title="Network Request"
|
||||
isDisplayed={current != null}
|
||||
content={
|
||||
current && (
|
||||
<FetchDetails
|
||||
resource={current}
|
||||
nextClick={onNextClick}
|
||||
prevClick={onPrevClick}
|
||||
first={currentIndex === 0}
|
||||
last={currentIndex === player.lists[NETWORK].countNow - 1}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
<TimeTable
|
||||
rows={player.lists[NETWORK].listNow}
|
||||
hoverable
|
||||
tableHeight={270}
|
||||
onRowClick={onRowClick}
|
||||
>
|
||||
{COLUMNS}
|
||||
</TimeTable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Network);
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
.popupNameTrigger {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
width: fit-content;
|
||||
}
|
||||
.popupNameContent {
|
||||
max-width: 600px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { PERFORMANCE } from 'Player/ios/state';
|
||||
|
||||
import * as PanelLayout from '../Layout/ToolPanel/PanelLayout';
|
||||
import Performance from '../Layout/ToolPanel/Performance';
|
||||
|
||||
|
||||
function Info({ name='', value='', show, className }) {
|
||||
if (!show) return null;
|
||||
return (
|
||||
<div className={cn("mr-3", className)}>
|
||||
<span>{name}</span>
|
||||
<b >{value}</b>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default observer(({ player }) => {
|
||||
const current = player.lists[PERFORMANCE].current;
|
||||
return (
|
||||
<>
|
||||
<PanelLayout.Header>
|
||||
<Info
|
||||
name="Thermal State"
|
||||
value={current && current.thermalState}
|
||||
show={current && ["serious", "critical"].includes(current.thermalState)}
|
||||
className={current && current.thermalState==="critical" ? "color-red" : "color-orange"}
|
||||
/>
|
||||
<Info
|
||||
name={current && current.activeProcessorCount}
|
||||
value="Active Processors"
|
||||
show={current && current.activeProcessorCount != null}
|
||||
/>
|
||||
<Info
|
||||
value="LOW POWER MODE"
|
||||
show={current && current.isLowPowerModeEnabled}
|
||||
className="color-red"
|
||||
/>
|
||||
|
||||
</PanelLayout.Header>
|
||||
<PanelLayout.Body>
|
||||
<Performance
|
||||
performanceChartTime={ current ? current.tmie : 0 }
|
||||
performanceChartData={ player.lists[PERFORMANCE].list }
|
||||
availability={ player.lists[PERFORMANCE].availability }
|
||||
hiddenScreenMarker={ false }
|
||||
player={ player }
|
||||
/>
|
||||
</PanelLayout.Body>
|
||||
</>
|
||||
);
|
||||
})
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import cn from 'classnames';
|
||||
|
||||
import { Loader, CircularLoader } from 'UI';
|
||||
|
||||
|
||||
function ScreenWithLoaders({ player, screenId, className }) {
|
||||
return (
|
||||
<div className={ className } id={screenId}>
|
||||
<div className={
|
||||
cn("absolute inset-0", {
|
||||
"opacity-75 bg-gray-light": player.loading || player.buffering,
|
||||
"bg-transparent": !(player.loading || player.buffering),
|
||||
})}
|
||||
>
|
||||
<Loader loading={ player.loading }>
|
||||
<CircularLoader loading={ player.buffering } inline={false} size="large"/>
|
||||
</Loader>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default observer(ScreenWithLoaders);
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { CUSTOM } from 'Player/ios/state';
|
||||
|
||||
import StackEvents from '../Layout/ToolPanel/StackEvents';
|
||||
|
||||
export default observer(({ player }) => <StackEvents stackEvents={player.lists[CUSTOM].listNow} />);
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { EVENTS } from 'Player/ios/state';
|
||||
import EventsBlock from 'Components/Session_/EventsBlock';
|
||||
|
||||
function Events({ style, player }) {
|
||||
return (
|
||||
<EventsBlock
|
||||
style={style}
|
||||
playing={ player.playing }
|
||||
player={ player }
|
||||
currentTimeEventIndex={ player.lists[EVENTS].countNow - 1 }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default observer(Events);
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { browserIcon, osIcon, deviceTypeIcon } from 'App/iconNames';
|
||||
import { formatTimeOrDate } from 'App/date';
|
||||
import { sessions as sessionsRoute } from 'App/routes';
|
||||
import { CountryFlag, IconButton, BackLink } from 'UI';
|
||||
import { toggleFavorite } from 'Duck/sessions';
|
||||
import { fetchList as fetchListIntegration } from 'Duck/integrations/actions';
|
||||
import SharePopup from 'Shared/SharePopup/SharePopup';
|
||||
import { capitalize } from "App/utils";
|
||||
|
||||
import Section from './Header/Section';
|
||||
import Resolution from './Header/Resolution';
|
||||
import Issues from 'Components/Session_/Issues/Issues'; //TODO replace folder
|
||||
import cls from './header.module.css';
|
||||
|
||||
|
||||
const SESSIONS_ROUTE = sessionsRoute();
|
||||
|
||||
|
||||
function Header({
|
||||
player,
|
||||
session,
|
||||
loading,
|
||||
isLocalUTC,
|
||||
toggleFavorite,
|
||||
favoriteLoading,
|
||||
fetchListIntegration,
|
||||
enableIssues,
|
||||
}) {
|
||||
useEffect(() => {
|
||||
fetchListIntegration('issues');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={ cn(cls.header, "flex") }>
|
||||
<BackLink to={ SESSIONS_ROUTE } label="Back" />
|
||||
|
||||
<div className={ cls.divider } />
|
||||
|
||||
<div className="mx-4 flex items-center">
|
||||
<CountryFlag country={ session.userCountry } />
|
||||
<div className="ml-2 font-normal color-gray-dark mt-1 text-sm">
|
||||
{ formatTimeOrDate(session.startedAt) } <span>{ isLocalUTC ? 'UTC' : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ !session.isIOS &&
|
||||
<Section icon={ browserIcon(session.userBrowser) } label={ `v${ session.userBrowserVersion }` } />
|
||||
}
|
||||
<Section icon={ deviceTypeIcon(session.userDeviceType) } label={ capitalise(session.userDevice) } />
|
||||
{ !session.isIOS &&
|
||||
<Section icon="expand-wide" label={ <Resolution player={player} /> } />
|
||||
}
|
||||
<Section icon={ osIcon(session.userOs) } label={ session.isIOS ? session.userOsVersion : session.userOs } />
|
||||
|
||||
<div className='ml-auto flex items-center'>
|
||||
<IconButton
|
||||
className="mr-2"
|
||||
tooltip="Bookmark"
|
||||
onClick={ toggleFavorite }
|
||||
loading={ favoriteLoading }
|
||||
icon={ session.favorite ? 'star-solid' : 'star' }
|
||||
plain
|
||||
/>
|
||||
<SharePopup
|
||||
entity="sessions"
|
||||
id={ session.sessionId }
|
||||
trigger={
|
||||
<IconButton
|
||||
className="mr-2"
|
||||
tooltip="Share Session"
|
||||
disabled={ loading }
|
||||
icon={ 'share-alt' }
|
||||
plain
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{ enableIssues && <Issues sessionId={ sessionId } /> }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default connect((state, props) => ({
|
||||
session: state.getIn([ 'sessions', 'current' ]),
|
||||
favoriteLoading: state.getIn([ 'sessions', 'toggleFavoriteRequest', 'loading' ]),
|
||||
loading: state.getIn([ 'sessions', 'loading' ]),
|
||||
enableIssues: !!state.getIn([ 'issues', 'list', 'token' ]), //??
|
||||
isLocalUTC: state.getIn(['sessions', 'timezone']) === 'UTC',
|
||||
}), {
|
||||
toggleFavorite, fetchListIntegration
|
||||
})(Header)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Icon } from 'UI';
|
||||
|
||||
|
||||
function Resolution ({ player }) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{ player.state.width || 'x' } <Icon name="close" size="12" className="mx-1" /> { player.state.height || 'x' }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default observer(Resolution);
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
|
||||
export default function Section({ icon, label }) {
|
||||
return (
|
||||
<div className="flex items-center mx-4">
|
||||
<Icon name={ icon } size="18" color="color-dark" />
|
||||
<div className="ml-2 mt-1 font-sm font-normal color-gray-darkest text-sm">{ label }</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
.wrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 40px;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: $gray-medium;
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { EscapeButton } from 'UI';
|
||||
|
||||
import Header from './Header';
|
||||
import ToolPanel from './ToolPanel';
|
||||
import PlayOverlay from './PlayOverlay';
|
||||
import Controls from './Player/Controls';
|
||||
|
||||
function Layout({ children, player, toolbar }) {
|
||||
return (
|
||||
<div className="flex flex-col h-screen">
|
||||
{!player.fullscreen.enabled && <Header player={player} />}
|
||||
<div className="flex-1 flex">
|
||||
<div
|
||||
className="flex flex-col"
|
||||
>
|
||||
<div className="flex-1 flex flex-col relative bg-white border-gray-light">
|
||||
{player.fullscreen.enabled && <EscapeButton onClose={player.toggleFullscreen} />}
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
{children}
|
||||
<PlayOverlay player={player} />
|
||||
</div>
|
||||
<Controls player={player} toolbar={toolbar} />
|
||||
</div>
|
||||
{!player.fullscreen.enabled && <ToolPanel player={player} toolbar={toolbar} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Layout);
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import cls from './PlayOverlay.module.css';
|
||||
|
||||
export default function PlayOverlay({ player }) {
|
||||
const [ iconVisible, setIconVisible ] = useState(false);
|
||||
|
||||
const togglePlay = useCallback(() => {
|
||||
player.togglePlay();
|
||||
setIconVisible(true);
|
||||
setTimeout(() => setIconVisible(false), 800);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
onClick={ togglePlay }
|
||||
>
|
||||
<div className={ cn("flex items-center justify-center", cls.iconWrapper, { [ cls.zoomWrapper ]: iconVisible }) } >
|
||||
<Icon name={ player.state.playing ? "play" : "pause"} size="30" color="gray-medium"/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
.iconWrapper {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transition: all .2s; /* Animation */
|
||||
}
|
||||
|
||||
.zoomWrapper {
|
||||
opacity: 1;
|
||||
transform: scale(1.8);
|
||||
transition: all .8s;
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import stl from './controlButton.module.css';
|
||||
|
||||
export default function ControlButton({ label, icon, onClick, disabled=false, count = 0, hasErrors=false, active=false }) {
|
||||
return (
|
||||
<button
|
||||
className={ cn(stl.controlButton, { [stl.disabled]: disabled, [stl.active]: active }) }
|
||||
onClick={ onClick }
|
||||
>
|
||||
<div className="relative">
|
||||
{ count > 0 && <div className={ stl.countLabel }>{ count }</div>}
|
||||
{ hasErrors && <div className={ stl.errorSymbol } /> }
|
||||
<Icon name={ icon } size="20" color="gray-dark"/>
|
||||
</div>
|
||||
<span className={ stl.label }>{ label }</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useEffect } from 'react';
|
||||
import cn from 'classnames';
|
||||
|
||||
import Timeline from './Timeline';
|
||||
import ControlButton from './ControlButton';
|
||||
|
||||
import cls from './Controls.module.css';
|
||||
|
||||
|
||||
|
||||
function getPlayButtonProps(player) {
|
||||
let label;
|
||||
let icon;
|
||||
if (player.state.completed) {
|
||||
label = 'Replay';
|
||||
icon = 'redo';
|
||||
} else if (player.state.playing) {
|
||||
label = 'Pause';
|
||||
icon = 'pause';
|
||||
} else {
|
||||
label = 'Play';
|
||||
icon = 'play';
|
||||
}
|
||||
return {
|
||||
label,
|
||||
icon,
|
||||
};
|
||||
}
|
||||
|
||||
function Controls({
|
||||
player,
|
||||
toolbar,
|
||||
}) {
|
||||
useEffect(() => {
|
||||
function onKeyDown(e) {
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Esc' || e.key === 'Escape') {
|
||||
player.fullscreen.disable();
|
||||
}
|
||||
if (player.controlsDisabled) {
|
||||
return;
|
||||
}
|
||||
if (e.key === ' ') {
|
||||
document.activeElement.blur();
|
||||
player.togglePlay();
|
||||
}
|
||||
if (e.key === "ArrowRight") {
|
||||
player.forthTenSeconds();
|
||||
}
|
||||
if (e.key === "ArrowLeft") {
|
||||
player.backTenSeconds();
|
||||
}
|
||||
if (e.key === "ArrowDown") {
|
||||
player.speedDown();
|
||||
}
|
||||
if (e.key === "ArrowUp") {
|
||||
player.speedUp();
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const disabled = player.controlsDisabled;
|
||||
return (
|
||||
<div className={ cls.controls }>
|
||||
<Timeline player={ player } />
|
||||
{ !player.isFullscreen &&
|
||||
<div className={ cn("flex justify-between items-center", cls.buttons) } >
|
||||
<div className="flex items-center">
|
||||
<ControlButton
|
||||
disabled={ disabled }
|
||||
onClick={ player.togglePlay }
|
||||
{ ...getPlayButtonProps(player) }
|
||||
/>
|
||||
<ControlButton
|
||||
onClick={ player.backTenSeconds }
|
||||
disabled={ disabled }
|
||||
label="Back"
|
||||
icon="replay-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className={ cn("text-gray-darkest hover:bg-gray-lightest", cls.speedButton) }
|
||||
onClick={ player.toggleSpeed }
|
||||
data-disabled={ disabled }
|
||||
>
|
||||
<div>{ player.state.speed + 'x' }</div>
|
||||
</button>
|
||||
{/* <div className={ cls.divider } /> */}
|
||||
{/* <button */}
|
||||
{/* className={ cn("flex items-center text-gray-darkest hover:bg-gray-lightest", cls.skipIntervalButton) } */}
|
||||
{/* onClick={ player.toggleSkip } */}
|
||||
{/* data-disabled={ disabled } */}
|
||||
{/* > */}
|
||||
{/* { player.isSkippingInactivity && <Icon name="check" color="gray-dark" /> } */}
|
||||
{/* { 'Skip Inactivity' } */}
|
||||
{/* </button> */}
|
||||
<div className={ cls.divider } />
|
||||
{ toolbar.map(({ key, label, icon, hasErrors=false, showCount = true }) =>
|
||||
<ControlButton
|
||||
key={ key }
|
||||
disabled={ disabled || player.lists[key].count === 0 }
|
||||
onClick={ () => player.togglePanel(key) }
|
||||
active={ player.toolPanel.key === key }
|
||||
label={ label }
|
||||
icon={ icon }
|
||||
count={ showCount && player.lists[key].countNow }
|
||||
hasErrors={ hasErrors }
|
||||
/>
|
||||
)}
|
||||
|
||||
<ControlButton
|
||||
disabled={ disabled }
|
||||
onClick={ player.toggleFullscreen }
|
||||
active={ player.fullscreen.enabled }
|
||||
label="Full Screen"
|
||||
icon="fullscreen"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Controls);
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
@keyframes fade {
|
||||
0% { opacity: 1}
|
||||
50% { opacity: 0}
|
||||
100% { opacity: 1}
|
||||
}
|
||||
|
||||
.controls {
|
||||
border-top: solid thin $gray-light;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 7px;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
|
||||
.speedButton {
|
||||
font-size: 14px;
|
||||
padding: 0 10px;
|
||||
height: 30px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
|
||||
.skipIntervalButton {
|
||||
transition: all 0.2s;
|
||||
font-size: 14px;
|
||||
padding: 0 10px;
|
||||
height: 30px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 30px;
|
||||
width: 1px;
|
||||
margin: 0 5px;
|
||||
background-color: $gray-light-shade;
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Duration } from 'luxon';
|
||||
import styles from './playerTime.module.css';
|
||||
|
||||
function PlayerTime({ player, timeKey }) {
|
||||
return (
|
||||
<div className={ styles.time }>
|
||||
{ Duration.fromMillis(player.state[timeKey]).toFormat('m:ss') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(PlayerTime);
|
||||
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import cls from './timeTracker.module.css';
|
||||
|
||||
|
||||
function TimeTracker({ player, scale }) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={ cls.positionTracker }
|
||||
style={ { left: `${ player.state.time * scale }%` } }
|
||||
/>
|
||||
<div
|
||||
className={ cls.playedTimeline }
|
||||
style={ { width: `${ player.state.time * scale }%` } }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(TimeTracker);
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Tooltip } from 'UI';
|
||||
import { CRASHES, EVENTS } from 'Player/ios/state';
|
||||
import TimeTracker from './TimeTracker';
|
||||
import PlayerTime from './PlayerTime';
|
||||
import cls from './timeline.module.css';
|
||||
|
||||
export default function Timeline({ player }) {
|
||||
const seekProgress = useCallback((e) => {
|
||||
if (player.controlsDisabled) {
|
||||
return;
|
||||
}
|
||||
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
|
||||
const time = Math.max(Math.round(p * player.state.endTime), 0);
|
||||
player.jump(time);
|
||||
});
|
||||
const scale = 100 / player.state.endTime;
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<PlayerTime player={player} timeKey="time" />
|
||||
<div className={cn(cls.progress, 'relative flex items-center')} onClick={seekProgress}>
|
||||
<TimeTracker player={player} scale={scale} />
|
||||
<div className={cn('flex items-center', cls.timeline)} />
|
||||
{player.lists[EVENTS].list.map((e) => (
|
||||
<div key={e.key} className={cls.event} style={{ left: `${e.time * scale}%` }} />
|
||||
))}
|
||||
{player.lists[CRASHES].list.map((e) => (
|
||||
<Tooltip
|
||||
key={e.key}
|
||||
className="error"
|
||||
title={
|
||||
<div className={cls.popup}>
|
||||
<b>{`Crash ${e.name}:`}</b>
|
||||
<br />
|
||||
<span>{e.reason}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
key={e.key}
|
||||
className={cn(cls.markup, cls.error)}
|
||||
style={{ left: `${e.time * scale}%` }}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<PlayerTime player={player} timeKey="endTime" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
|
||||
.controlButton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
min-width: 60px;
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
&.active, &:hover {
|
||||
background-color: $gray-lightest;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
& .errorSymbol {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background-color: red;
|
||||
top: 12px;
|
||||
left: 23px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
& .countLabel {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 12px;
|
||||
background-color: $gray-dark;
|
||||
color: white;
|
||||
font-size: 9px;
|
||||
font-weight: 300;
|
||||
min-width: 20px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
& .label {
|
||||
/* padding-top: 5px; */
|
||||
font-size: 10px;
|
||||
color: $gray-darkest;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
.time {
|
||||
padding: 0 12px;
|
||||
color: $gray-medium;
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
@import 'zindex.css';
|
||||
|
||||
.positionTracker {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border: solid 1px $teal;
|
||||
margin-left: -7px;
|
||||
border-radius: 50%;
|
||||
background-color: $active-blue;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: $positionTracker;
|
||||
pointer-events: none; /* temporary. DnD should be */
|
||||
}
|
||||
|
||||
.playedTimeline {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
background-color: $teal;
|
||||
pointer-events: none;
|
||||
height: 2px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
.progress {
|
||||
height: 10px;
|
||||
border-radius: 1px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: $gray-light;
|
||||
}
|
||||
|
||||
|
||||
.event {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border: solid 1px white;
|
||||
margin-left: -4px;
|
||||
border-radius: 50%;
|
||||
background: rgba(136, 136, 136, 0.8);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.popup {
|
||||
max-width: 300px !important;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
& span {
|
||||
display: block;
|
||||
max-height: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.markup {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.markup.log {
|
||||
background: $blue;
|
||||
}
|
||||
|
||||
.markup.error {
|
||||
background: $red;
|
||||
}
|
||||
|
||||
.markup.warning {
|
||||
background: $orange;
|
||||
}
|
||||
|
||||
.markup.info {
|
||||
background: $blue2;
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { CloseButton } from 'UI';
|
||||
|
||||
|
||||
function ToolPanel({ toolbar, player }) {
|
||||
const currentKey = player.toolPanel.key;
|
||||
const tool = toolbar.find(p => p.key === currentKey);
|
||||
if (!tool) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="relative bg-white mb-2 border-gray-light"
|
||||
style={{ height: '300px' }} // Using style is ok for the unique-on-page elements
|
||||
>
|
||||
<CloseButton onClick={ player.closePanel } size="18" className="absolute top-0 right-0 z-10 p-2 bg-white rounded-full border opacity-25 hover:opacity-100" />
|
||||
<tool.Component player={ player } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(ToolPanel);
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
|
||||
import cls from './Log.module.css';
|
||||
|
||||
|
||||
function getIconProps(level) {
|
||||
switch (level) {
|
||||
case "info":
|
||||
return {
|
||||
name: 'console/info',
|
||||
color: 'blue2',
|
||||
};
|
||||
case "warn":
|
||||
return {
|
||||
name: 'console/warning',
|
||||
color: 'red2',
|
||||
};
|
||||
case "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>)
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
level is "info"/"warn"/"error"
|
||||
*/
|
||||
export default function Log({ text, level, onClick }) {
|
||||
return (
|
||||
<div
|
||||
className={ cn("flex items-start font-mono cursor-pointer border-b border-gray-light-shade", cls.line, level) }
|
||||
data-scroll-item={ level === "error" }
|
||||
onClick={ onClick }
|
||||
>
|
||||
<Icon size="14" className="pt-1" { ...getIconProps(level) } />
|
||||
<div className={ cn("overflow-y-auto ml-20", cls.message)}>{ renderWithNL(text) }</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
.line {
|
||||
padding: 7px 0 7px 15px;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 13px;
|
||||
&::-webkit-scrollbar {
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
|
||||
export function Header({
|
||||
children,
|
||||
className,
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={ cn("flex items-center justify-between border-bottom-gray-light pr-10 pl-2", className) }
|
||||
style={{ height: "14%" }}
|
||||
>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function Body({ children }) {
|
||||
return (
|
||||
<div style={{ height: "86%" }}>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,631 +0,0 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
ComposedChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
ReferenceLine,
|
||||
Label,
|
||||
} from 'recharts';
|
||||
import { durationFromMsFormatted } from 'App/date';
|
||||
import { formatBytes } from 'App/utils';
|
||||
|
||||
|
||||
const tooltipWrapperClass = "bg-white rounded border px-2 py-1";
|
||||
|
||||
const CPU_VISUAL_OFFSET = 10;
|
||||
|
||||
const FPS_COLOR = '#C5E5E7';
|
||||
const FPS_STROKE_COLOR = "#92C7CA";
|
||||
const FPS_LOW_COLOR = "pink";
|
||||
const FPS_VERY_LOW_COLOR = "red";
|
||||
const CPU_COLOR = "#A8D1DE";
|
||||
const CPU_STROKE_COLOR = "#69A5B8";
|
||||
const BATTERY_COLOR = "orange";
|
||||
const BATTERY_STROKE_COLOR = "orange";
|
||||
const USED_HEAP_COLOR = '#A9ABDC';
|
||||
const USED_HEAP_STROKE_COLOR = "#8588CF";
|
||||
const TOTAL_HEAP_STROKE_COLOR = '#4A4EB7';
|
||||
const NODES_COUNT_COLOR = "#C6A9DC";
|
||||
const NODES_COUNT_STROKE_COLOR = "#7360AC";
|
||||
const HIDDEN_SCREEN_COLOR = "#CCC";
|
||||
|
||||
|
||||
const CURSOR_COLOR = "#394EFF";
|
||||
|
||||
const Gradient = ({ color, id }) => (
|
||||
<linearGradient id={ id } x1="-1" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={ color } stopOpacity={ 0.7 } />
|
||||
<stop offset="95%" stopColor={ color } stopOpacity={ 0.2 } />
|
||||
</linearGradient>
|
||||
);
|
||||
|
||||
|
||||
const TOTAL_HEAP = "Allocated Heap";
|
||||
const USED_HEAP = "JS Heap";
|
||||
const FPS = "Framerate";
|
||||
const CPU = "CPU Load";
|
||||
const MEMORY = "Memory Usage";
|
||||
const BATTERY = "Battery Charge";
|
||||
const NODES_COUNT = "Nodes Сount";
|
||||
|
||||
|
||||
const FPSTooltip = ({ active, payload }) => {
|
||||
if (!active || payload.length < 3) {
|
||||
return null;
|
||||
}
|
||||
if (payload[0].value === null) {
|
||||
return (
|
||||
<div className={ tooltipWrapperClass } style={{ color: HIDDEN_SCREEN_COLOR }}>
|
||||
{"Page is not active. User switched the tab or hid the window."}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let style;
|
||||
if (payload[1].value != null && payload[1].value > 0) {
|
||||
style = { color: FPS_LOW_COLOR };
|
||||
}
|
||||
if (payload[2].value != null && payload[2].value > 0) {
|
||||
style = { color: FPS_VERY_LOW_COLOR };
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ tooltipWrapperClass } style={ style }>
|
||||
<span className="font-medium">{`${ FPS }: `}</span>
|
||||
{ Math.trunc(payload[0].value) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CPUTooltip = ({ active, payload }) => {
|
||||
if (!active || payload.length < 1 || payload[0].value === null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={ tooltipWrapperClass } >
|
||||
<span className="font-medium">{`${ CPU }: `}</span>
|
||||
{ payload[0].value - CPU_VISUAL_OFFSET }
|
||||
{"%"}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const HeapTooltip = ({ active, payload }) => {
|
||||
if (!active || payload.length < 2) return null;
|
||||
return (
|
||||
<div className={ tooltipWrapperClass } >
|
||||
<p>
|
||||
<span className="font-medium">{`${ TOTAL_HEAP }: `}</span>
|
||||
{ formatBytes(payload[0].value)}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">{`${ USED_HEAP }: `}</span>
|
||||
{ formatBytes(payload[1].value)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MemoryTooltip = ({ active, payload}) => {
|
||||
if (!active || payload.length < 1) return null;
|
||||
return (
|
||||
<div className={ tooltipWrapperClass } >
|
||||
<span className="font-medium">{`${ MEMORY }: `}</span>
|
||||
{ formatBytes(payload[0].value)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BatteryTooltip = ({ active, payload}) => {
|
||||
if (!active || payload.length < 1 || payload[0].value === null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={ tooltipWrapperClass } >
|
||||
<span className="font-medium">{`${ BATTERY }: `}</span>
|
||||
{ payload[0].value }
|
||||
{"%"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const NodesCountTooltip = ({ active, payload } ) => {
|
||||
if (!active || payload.length === 0) return null;
|
||||
return (
|
||||
<div className={ tooltipWrapperClass } >
|
||||
<p>
|
||||
<span className="font-medium">{`${ NODES_COUNT }: `}</span>
|
||||
{ payload[0].value }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const TICKS_COUNT = 10;
|
||||
function generateTicks(data: Array<Timed>): Array<number> {
|
||||
if (data.length === 0) return [];
|
||||
const minTime = data[0].time;
|
||||
const maxTime = data[data.length-1].time;
|
||||
|
||||
const ticks = [];
|
||||
const tickGap = (maxTime - minTime) / (TICKS_COUNT + 1);
|
||||
for (let i = 0; i < TICKS_COUNT; i++) {
|
||||
const tick = tickGap * (i + 1) + minTime;
|
||||
ticks.push(tick);
|
||||
}
|
||||
return ticks;
|
||||
}
|
||||
|
||||
const LOW_FPS = 30;
|
||||
const VERY_LOW_FPS = 20;
|
||||
const LOW_FPS_MARKER_VALUE = 5;
|
||||
const HIDDEN_SCREEN_MARKER_VALUE = 20;
|
||||
function adaptForGraphics(data) {
|
||||
return data.map((point, i) => {
|
||||
let fpsVeryLowMarker = null;
|
||||
let fpsLowMarker = null;
|
||||
let hiddenScreenMarker = 0;
|
||||
if (point.fps != null) {
|
||||
fpsVeryLowMarker = 0;
|
||||
fpsLowMarker = 0;
|
||||
if (point.fps < VERY_LOW_FPS) {
|
||||
fpsVeryLowMarker = LOW_FPS_MARKER_VALUE;
|
||||
} else if (point.fps < LOW_FPS) {
|
||||
fpsLowMarker = LOW_FPS_MARKER_VALUE;
|
||||
}
|
||||
}
|
||||
if (point.fps == null ||
|
||||
(i > 0 && data[i - 1].fps == null) //||
|
||||
//(i < data.length-1 && data[i + 1].fps == null)
|
||||
) {
|
||||
hiddenScreenMarker = HIDDEN_SCREEN_MARKER_VALUE;
|
||||
}
|
||||
if (point.cpu != null) {
|
||||
point.cpu += CPU_VISUAL_OFFSET;
|
||||
}
|
||||
return {
|
||||
...point,
|
||||
fpsLowMarker,
|
||||
fpsVeryLowMarker,
|
||||
hiddenScreenMarker,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default class Performance extends React.PureComponent {
|
||||
_timeTicks = generateTicks(this.props.performanceChartData)
|
||||
_data = adaptForGraphics(this.props.performanceChartData)
|
||||
|
||||
onDotClick = ({ index }) => {
|
||||
const point = this._data[index];
|
||||
if (!!point) {
|
||||
this.props.player.jump(point.time);
|
||||
}
|
||||
}
|
||||
|
||||
onChartClick = (e) => {
|
||||
if (e === null) return;
|
||||
const { activeTooltipIndex } = e;
|
||||
const point = this._data[activeTooltipIndex];
|
||||
if (!!point) {
|
||||
this.props.player.jump(point.time);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
performanceChartTime,
|
||||
availability = {},
|
||||
hiddenScreenMarker = true,
|
||||
} = this.props;
|
||||
const { fps, cpu, heap, nodes, memory, battery } = availability;
|
||||
const availableCount = [ fps, cpu, heap, nodes, memory, battery ].reduce((c, av) => av ? c + 1 : c, 0);
|
||||
const height = availableCount === 0 ? "0" : `${100 / availableCount}%`;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ fps &&
|
||||
<ResponsiveContainer height={ height }>
|
||||
<AreaChart
|
||||
onClick={ this.onChartClick }
|
||||
data={this._data}
|
||||
syncId="s"
|
||||
margin={{
|
||||
top: 0, right: 0, left: 0, bottom: 0,
|
||||
}}
|
||||
>
|
||||
<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" }}
|
||||
domain={[0, 'dataMax']}
|
||||
ticks={this._timeTicks}
|
||||
>
|
||||
<Label value="FPS" position="insideRight" className="fill-gray-medium" />
|
||||
</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 }
|
||||
/>
|
||||
{ hiddenScreenMarker &&
|
||||
<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,
|
||||
}}
|
||||
>
|
||||
<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="insideRight" className="fill-gray-medium" />
|
||||
</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 }
|
||||
/>
|
||||
{ hiddenScreenMarker &&
|
||||
<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>
|
||||
}
|
||||
{ battery &&
|
||||
<ResponsiveContainer height={ height }>
|
||||
<AreaChart
|
||||
onClick={ this.onChartClick }
|
||||
data={this._data}
|
||||
syncId="s"
|
||||
margin={{
|
||||
top: 0, right: 0, left: 0, bottom: 0,
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<Gradient id="batteryGrad" color={ BATTERY_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="BATTERY" position="insideTopRight" className="fill-gray-medium" />
|
||||
</XAxis>
|
||||
<YAxis
|
||||
axisLine={ false }
|
||||
tick={ false }
|
||||
mirror
|
||||
domain={[ 0, 120]}
|
||||
orientation="right"
|
||||
/>
|
||||
<Area
|
||||
dataKey="battery"
|
||||
type="monotone"
|
||||
stroke={BATTERY_STROKE_COLOR}
|
||||
// fill="none"
|
||||
fill="url(#batteryGrad)"
|
||||
dot={false}
|
||||
activeDot={{
|
||||
onClick: this.onDotClick,
|
||||
style: { cursor: "pointer" },
|
||||
}}
|
||||
isAnimationActive={ false }
|
||||
/>
|
||||
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
|
||||
<Tooltip
|
||||
content={BatteryTooltip}
|
||||
filterNull={ false }
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
}
|
||||
{ memory &&
|
||||
<ResponsiveContainer height={ height }>
|
||||
<AreaChart
|
||||
onClick={ this.onChartClick }
|
||||
data={this._data}
|
||||
syncId="s"
|
||||
margin={{
|
||||
top: 0, right: 0, left: 0, bottom: 0,
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<Gradient id="usedHeapGradient" color={ USED_HEAP_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="MEMORY" position="insideTopRight" className="fill-gray-medium" />
|
||||
</XAxis>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickFormatter={formatBytes}
|
||||
mirror
|
||||
// Hack to keep only end tick
|
||||
minTickGap={Number.MAX_SAFE_INTEGER}
|
||||
domain={[0, max => max*1.2]}
|
||||
/>
|
||||
<Area
|
||||
dataKey="memory"
|
||||
type="monotone"
|
||||
stroke={USED_HEAP_STROKE_COLOR}
|
||||
// fill="none"
|
||||
fill="url(#usedHeapGradient)"
|
||||
dot={false}
|
||||
activeDot={{
|
||||
onClick: this.onDotClick,
|
||||
style: { cursor: "pointer" },
|
||||
}}
|
||||
isAnimationActive={ false }
|
||||
/>
|
||||
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
|
||||
<Tooltip
|
||||
content={MemoryTooltip}
|
||||
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"
|
||||
>
|
||||
<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="insideRight" className="fill-gray-medium" />
|
||||
</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,
|
||||
}}
|
||||
>
|
||||
<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="insideRight" className="fill-gray-medium" />
|
||||
</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>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { useState } from 'react';
|
||||
import { NoContent, Tabs } from 'UI';
|
||||
import { hideHint } from 'Duck/components/player';
|
||||
import { typeList } from 'Types/session/stackEvent';
|
||||
import * as PanelLayout from './PanelLayout';
|
||||
|
||||
import UserEvent from 'Components/Session_/StackEvents/UserEvent';
|
||||
import Autoscroll from 'Components/Session_/Autoscroll';
|
||||
|
||||
const ALL = 'ALL';
|
||||
|
||||
const TABS = [ ALL, ...typeList ].map(tab =>({ text: tab, key: tab }));
|
||||
|
||||
function StackEvents({
|
||||
stackEvents,
|
||||
hintIsHidden,
|
||||
hideHint,
|
||||
}) {
|
||||
const [ activeTab, setTab ] = useState(ALL);
|
||||
const tabs = TABS.filter(({ key }) => key === ALL || stackEvents.some(({ source }) => key === source)); // Do it once for all when we show them all
|
||||
|
||||
const filteredStackEvents = stackEvents
|
||||
.filter(({ source }) => activeTab === ALL || activeTab === source);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelLayout.Header>
|
||||
<Tabs
|
||||
className="uppercase"
|
||||
tabs={ tabs }
|
||||
active={ activeTab }
|
||||
onClick={ setTab }
|
||||
border={ false }
|
||||
/>
|
||||
</PanelLayout.Header>
|
||||
<PanelLayout.Body>
|
||||
<NoContent
|
||||
title="Nothing to display yet"
|
||||
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>
|
||||
{ ' 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>
|
||||
</>
|
||||
: null
|
||||
}
|
||||
size="small"
|
||||
show={ filteredStackEvents.length === 0 }
|
||||
>
|
||||
<Autoscroll>
|
||||
{ filteredStackEvents.map(userEvent => (
|
||||
<UserEvent key={ userEvent.key } userEvent={ userEvent }/>
|
||||
))}
|
||||
</Autoscroll>
|
||||
</NoContent>
|
||||
</PanelLayout.Body>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
hintIsHidden: state.getIn(['components', 'player', 'hiddenHints', 'stack']) ||
|
||||
!state.getIn([ 'site', 'list' ]).some(s => s.stackIntegrations),
|
||||
}), {
|
||||
hideHint
|
||||
})(StackEvents);
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
.header {
|
||||
height: 50px;
|
||||
border-bottom: solid thin $gray-light;
|
||||
padding: 10px 15px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
margin: 0 15px;
|
||||
background-color: $gray-light;
|
||||
}
|
||||
|
||||
|
|
@ -61,6 +61,7 @@ function SubHeader(props: any) {
|
|||
}
|
||||
/>
|
||||
<ItemMenu
|
||||
useSc
|
||||
items={menuItems}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ function SubHeader(props: any) {
|
|||
>
|
||||
<SummaryButton onClick={showSummary} />
|
||||
<NotePopup />
|
||||
<ItemMenu items={additionalMenu} />
|
||||
<ItemMenu items={additionalMenu} useSc />
|
||||
{uxtestingStore.isUxt() ? (
|
||||
<Switch
|
||||
checkedChildren={'DevTools'}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { countries } from 'App/constants';
|
|||
import { useStore } from 'App/mstore';
|
||||
import { browserIcon, osIcon, deviceTypeIcon } from 'App/iconNames';
|
||||
import { formatTimeOrDate } from 'App/date';
|
||||
import { Avatar, TextEllipsis, CountryFlag, Icon, Tooltip, Popover } from 'UI';
|
||||
import { Avatar, TextEllipsis, CountryFlag, Icon, Tooltip } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import { withRequest } from 'HOCs';
|
||||
import SessionInfoItem from 'Components/Session_/SessionInfoItem';
|
||||
|
|
@ -13,10 +13,12 @@ import { useModal } from 'App/components/Modal';
|
|||
import UserSessionsModal from 'Shared/UserSessionsModal';
|
||||
import { IFRAME } from 'App/constants/storageKeys';
|
||||
import { capitalize } from "App/utils";
|
||||
import { Popover } from 'antd'
|
||||
|
||||
function UserCard({ className, request, session, width, height, similarSessions, loading }) {
|
||||
const { settingsStore } = useStore();
|
||||
const { timezone } = settingsStore.sessionSettings;
|
||||
const [showMore, setShowMore] = React.useState(false)
|
||||
|
||||
const {
|
||||
userBrowser,
|
||||
|
|
@ -51,6 +53,23 @@ function UserCard({ className, request, session, width, height, similarSessions,
|
|||
const avatarbgSize = '38px';
|
||||
|
||||
const safeOs = userOs === 'IOS' ? 'iOS' : userOs;
|
||||
|
||||
React.useEffect(() => {
|
||||
const handler = (e) => {
|
||||
if (e.shiftKey) {
|
||||
e.preventDefault()
|
||||
if (e.key === 'I') {
|
||||
setShowMore(!showMore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handler, false)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handler)
|
||||
}
|
||||
}, [showMore])
|
||||
return (
|
||||
<div className={cn('bg-white flex items-center w-full', className)}>
|
||||
<div className="flex items-center">
|
||||
|
|
@ -84,7 +103,8 @@ function UserCard({ className, request, session, width, height, similarSessions,
|
|||
</span>
|
||||
<span className="mx-1 font-bold text-xl">·</span>
|
||||
<Popover
|
||||
render={() => (
|
||||
open={showMore ? true : undefined}
|
||||
content={() => (
|
||||
<div className="text-left bg-white rounded">
|
||||
<SessionInfoItem
|
||||
comp={<CountryFlag country={userCountry} height={11} />}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ function PlayerBlock(props: IProps) {
|
|||
? <AiSubheader sessionId={sessionId} disabled={disabled} jiraConfig={jiraConfig} activeTab={activeTab} setActiveTab={setActiveTab} />
|
||||
: <SubHeader sessionId={sessionId} disabled={disabled} jiraConfig={jiraConfig} />
|
||||
: null}
|
||||
<Player activeTab={activeTab} fullView={fullView} />
|
||||
<Player setActiveTab={setActiveTab} activeTab={activeTab} fullView={fullView} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ function PlayerBlockHeader(props: any) {
|
|||
<div className={stl.divider} />
|
||||
</div>
|
||||
)}
|
||||
<UserCard className="" width={width} height={height} />
|
||||
<UserCard width={width} height={height} />
|
||||
|
||||
<div className={cn('ml-auto flex items-center h-full', { hidden: closedLive })}>
|
||||
{live && !hideBack && !uxtestingStore.isUxt() && (
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ interface IProps {
|
|||
sessionId: string;
|
||||
activeTab: string;
|
||||
updateLastPlayedSession: (id: string) => void;
|
||||
setActiveTab: (tab: string) => void;
|
||||
}
|
||||
|
||||
export const heightKey = 'playerPanelHeight'
|
||||
|
|
@ -149,6 +150,7 @@ function Player(props: IProps) {
|
|||
)}
|
||||
{!fullView ? (
|
||||
<Controls
|
||||
setActiveTab={props.setActiveTab}
|
||||
speedDown={playerContext.player.speedDown}
|
||||
speedUp={playerContext.player.speedUp}
|
||||
jump={playerContext.player.jump}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import { SKIP_INTERVALS } from 'Components/Session_/Player/Controls/Controls';
|
||||
import { useEffect, useContext } from 'react';
|
||||
import { PlayerContext } from 'Components/Session/playerContext';
|
||||
import { blockValues, blocks } from 'Duck/components/player';
|
||||
|
||||
function useShortcuts({
|
||||
skipInterval,
|
||||
fullScreenOn,
|
||||
fullScreenOff,
|
||||
toggleBottomBlock,
|
||||
openNextSession,
|
||||
openPrevSession,
|
||||
setActiveTab,
|
||||
}: {
|
||||
skipInterval: keyof typeof SKIP_INTERVALS;
|
||||
fullScreenOn: () => void;
|
||||
fullScreenOff: () => void;
|
||||
toggleBottomBlock: (blockName: (typeof blockValues)[number]) => void;
|
||||
openNextSession: () => void;
|
||||
openPrevSession: () => void;
|
||||
setActiveTab: (tab: string) => void;
|
||||
}) {
|
||||
const { player } = useContext(PlayerContext);
|
||||
|
||||
const forthTenSeconds = () => {
|
||||
player.jumpInterval(SKIP_INTERVALS[skipInterval]);
|
||||
};
|
||||
|
||||
const backTenSeconds = () => {
|
||||
player.jumpInterval(-SKIP_INTERVALS[skipInterval]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
// shift + f = fullscreenOn
|
||||
if (e.shiftKey) {
|
||||
player.toggleInspectorMode(false)
|
||||
switch (e.key) {
|
||||
case 'F':
|
||||
return fullScreenOn();
|
||||
case 'X':
|
||||
return toggleBottomBlock(blocks.overview);
|
||||
case 'P':
|
||||
return toggleBottomBlock(blocks.performance);
|
||||
case 'N':
|
||||
return toggleBottomBlock(blocks.network);
|
||||
case 'C':
|
||||
return toggleBottomBlock(blocks.console);
|
||||
case 'R':
|
||||
return toggleBottomBlock(blocks.storage);
|
||||
case 'E':
|
||||
return toggleBottomBlock(blocks.stackEvents);
|
||||
case '>':
|
||||
return openNextSession();
|
||||
case '<':
|
||||
return openPrevSession();
|
||||
case 'A':
|
||||
player.pause();
|
||||
return setActiveTab('EVENTS');
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (e.key === 'Esc' || e.key === 'Escape') {
|
||||
fullScreenOff();
|
||||
}
|
||||
if (e.key === ' ') {
|
||||
(document.activeElement as HTMLInputElement | null)?.blur?.();
|
||||
player.togglePlay();
|
||||
}
|
||||
if (e.key === 'ArrowRight') {
|
||||
forthTenSeconds();
|
||||
}
|
||||
if (e.key === 'ArrowLeft') {
|
||||
backTenSeconds();
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
player.speedDown();
|
||||
}
|
||||
if (e.key === 'ArrowUp') {
|
||||
player.speedUp();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [forthTenSeconds, backTenSeconds, player, fullScreenOn, fullScreenOff]);
|
||||
}
|
||||
|
||||
export default useShortcuts;
|
||||
|
|
@ -2,6 +2,24 @@ import React from 'react';
|
|||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import stl from './controlButton.module.css';
|
||||
import {Popover} from 'antd'
|
||||
|
||||
|
||||
interface IProps {
|
||||
label: string;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
count?: number;
|
||||
hasErrors?: boolean;
|
||||
active?: boolean;
|
||||
size?: number;
|
||||
noLabel?: boolean;
|
||||
labelClassName?: string;
|
||||
containerClassName?: string;
|
||||
noIcon?: boolean;
|
||||
popover?: React.ReactNode
|
||||
}
|
||||
|
||||
const ControlButton = ({
|
||||
label,
|
||||
|
|
@ -16,7 +34,9 @@ const ControlButton = ({
|
|||
labelClassName,
|
||||
containerClassName,
|
||||
noIcon,
|
||||
}) => (
|
||||
popover = undefined,
|
||||
}: IProps) => (
|
||||
<Popover content={popover} open={popover ? undefined : false}>
|
||||
<button
|
||||
className={cn(
|
||||
stl.controlButton,
|
||||
|
|
@ -40,6 +60,7 @@ const ControlButton = ({
|
|||
</span>
|
||||
)}
|
||||
</button>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
ControlButton.displayName = 'ControlButton';
|
||||
|
|
@ -1,11 +1,21 @@
|
|||
import { useStore } from "App/mstore";
|
||||
import { useStore } from 'App/mstore';
|
||||
import { session as sessionRoute, withSiteId } from 'App/routes';
|
||||
import KeyboardHelp, {
|
||||
LaunchConsoleShortcut,
|
||||
LaunchEventsShortcut,
|
||||
LaunchNetworkShortcut,
|
||||
LaunchPerformanceShortcut,
|
||||
LaunchStateShortcut,
|
||||
PlayPauseSessionShortcut,
|
||||
PlaySessionInFullscreenShortcut,
|
||||
} from 'Components/Session_/Player/Controls/components/KeyboardHelp';
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectStorageType, STORAGE_TYPES, StorageType } from 'Player';
|
||||
import { PlayButton, PlayingState, FullScreenButton } from 'App/player-ui'
|
||||
import { PlayButton, PlayingState, FullScreenButton } from 'App/player-ui';
|
||||
import { Popover } from 'antd';
|
||||
|
||||
import { Tooltip } from 'UI';
|
||||
import {
|
||||
CONSOLE,
|
||||
fullscreenOff,
|
||||
|
|
@ -32,6 +42,7 @@ import PlayerControls from './components/PlayerControls';
|
|||
import styles from './controls.module.css';
|
||||
import XRayButton from 'Shared/XRayButton';
|
||||
import CreateNote from 'Components/Session_/Player/Controls/components/CreateNote';
|
||||
import useShortcuts from 'Components/Session/Player/ReplayPlayer/useShortcuts';
|
||||
|
||||
export const SKIP_INTERVALS = {
|
||||
2: 2e3,
|
||||
|
|
@ -57,22 +68,17 @@ function getStorageName(type: any) {
|
|||
return 'ZUSTAND';
|
||||
case STORAGE_TYPES.NONE:
|
||||
return 'STATE';
|
||||
default:
|
||||
return 'STATE';
|
||||
}
|
||||
}
|
||||
|
||||
function Controls(props: any) {
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const { uxtestingStore } = useStore();
|
||||
|
||||
const {
|
||||
playing,
|
||||
completed,
|
||||
skip,
|
||||
speed,
|
||||
messagesLoading,
|
||||
markedTargets,
|
||||
inspectorMode,
|
||||
} = store.get();
|
||||
|
||||
const { playing, completed, skip, speed, messagesLoading, markedTargets, inspectorMode } =
|
||||
store.get();
|
||||
|
||||
const {
|
||||
bottomBlock,
|
||||
|
|
@ -83,42 +89,32 @@ function Controls(props: any) {
|
|||
disabledRedux,
|
||||
showStorageRedux,
|
||||
session,
|
||||
previousSessionId,
|
||||
nextSessionId,
|
||||
siteId,
|
||||
setActiveTab,
|
||||
} = props;
|
||||
|
||||
const disabled = disabledRedux || messagesLoading || inspectorMode || markedTargets;
|
||||
const sessionTz = session?.timezone;
|
||||
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();
|
||||
}
|
||||
|
||||
const nextHandler = () => {
|
||||
props.history.push(withSiteId(sessionRoute(nextSessionId), siteId));
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener('keydown', onKeyDown.bind(this));
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown.bind(this));
|
||||
};
|
||||
}, []);
|
||||
const prevHandler = () => {
|
||||
props.history.push(withSiteId(sessionRoute(previousSessionId), siteId));
|
||||
};
|
||||
|
||||
useShortcuts({
|
||||
skipInterval,
|
||||
fullScreenOn: props.fullscreenOn,
|
||||
fullScreenOff: props.fullscreenOff,
|
||||
toggleBottomBlock,
|
||||
openNextSession: nextHandler,
|
||||
openPrevSession: prevHandler,
|
||||
setActiveTab,
|
||||
});
|
||||
|
||||
const forthTenSeconds = () => {
|
||||
// @ts-ignore
|
||||
|
|
@ -131,11 +127,15 @@ function Controls(props: any) {
|
|||
};
|
||||
|
||||
const toggleBottomTools = (blockName: number) => {
|
||||
player.toggleInspectorMode(false);
|
||||
toggleBottomBlock(blockName);
|
||||
player.toggleInspectorMode(false);
|
||||
toggleBottomBlock(blockName);
|
||||
};
|
||||
|
||||
const state = completed ? PlayingState.Completed : playing ? PlayingState.Playing : PlayingState.Paused
|
||||
const state = completed
|
||||
? PlayingState.Completed
|
||||
: playing
|
||||
? PlayingState.Playing
|
||||
: PlayingState.Paused;
|
||||
|
||||
return (
|
||||
<div className={styles.controls}>
|
||||
|
|
@ -164,24 +164,24 @@ function Controls(props: any) {
|
|||
isActive={bottomBlock === OVERVIEW && !inspectorMode}
|
||||
onClick={() => toggleBottomTools(OVERVIEW)}
|
||||
/>
|
||||
<KeyboardHelp />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center h-full">
|
||||
{uxtestingStore.hideDevtools && uxtestingStore.isUxt() ? null :
|
||||
{uxtestingStore.hideDevtools && uxtestingStore.isUxt() ? null : (
|
||||
<DevtoolsButtons
|
||||
showStorageRedux={showStorageRedux}
|
||||
toggleBottomTools={toggleBottomTools}
|
||||
bottomBlock={bottomBlock}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
<Tooltip title="Fullscreen" delay={0} placement="top-start" className="mx-4">
|
||||
<FullScreenButton
|
||||
size={16}
|
||||
onClick={props.fullscreenOn}
|
||||
customClasses={'rounded hover:bg-gray-light-shade color-gray-medium'}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<FullScreenButton
|
||||
size={16}
|
||||
onClick={props.fullscreenOn}
|
||||
customClasses={'rounded hover:bg-gray-light-shade color-gray-medium'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -196,112 +196,142 @@ interface IDevtoolsButtons {
|
|||
disabled: boolean;
|
||||
}
|
||||
|
||||
const DevtoolsButtons = observer(({ showStorageRedux, toggleBottomTools, bottomBlock, disabled }: IDevtoolsButtons) => {
|
||||
const { store } = React.useContext(PlayerContext);
|
||||
const DevtoolsButtons = observer(
|
||||
({ showStorageRedux, toggleBottomTools, bottomBlock, disabled }: IDevtoolsButtons) => {
|
||||
const { store } = React.useContext(PlayerContext);
|
||||
|
||||
const {
|
||||
inspectorMode,
|
||||
currentTab,
|
||||
tabStates
|
||||
} = store.get();
|
||||
const { inspectorMode, currentTab, tabStates } = store.get();
|
||||
|
||||
const disableButtons = disabled;
|
||||
const disableButtons = disabled;
|
||||
|
||||
const profilesList = tabStates[currentTab]?.profilesList || [];
|
||||
const graphqlList = tabStates[currentTab]?.graphqlList || [];
|
||||
const logRedCount = tabStates[currentTab]?.logMarkedCountNow || 0;
|
||||
const resourceRedCount = tabStates[currentTab]?.resourceMarkedCountNow || 0;
|
||||
const stackRedCount = tabStates[currentTab]?.stackMarkedCountNow || 0;
|
||||
const exceptionsList = tabStates[currentTab]?.exceptionsList || [];
|
||||
const profilesList = tabStates[currentTab]?.profilesList || [];
|
||||
const graphqlList = tabStates[currentTab]?.graphqlList || [];
|
||||
const logRedCount = tabStates[currentTab]?.logMarkedCountNow || 0;
|
||||
const resourceRedCount = tabStates[currentTab]?.resourceMarkedCountNow || 0;
|
||||
const stackRedCount = tabStates[currentTab]?.stackMarkedCountNow || 0;
|
||||
const exceptionsList = tabStates[currentTab]?.exceptionsList || [];
|
||||
|
||||
const storageType = store.get().tabStates[currentTab] ? selectStorageType(store.get().tabStates[currentTab]) : StorageType.NONE
|
||||
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;
|
||||
return (
|
||||
<>
|
||||
<ControlButton
|
||||
disabled={disableButtons}
|
||||
onClick={() => toggleBottomTools(CONSOLE)}
|
||||
active={bottomBlock === CONSOLE && !inspectorMode}
|
||||
label="CONSOLE"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
hasErrors={logRedCount > 0 || showExceptions}
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
const storageType = store.get().tabStates[currentTab]
|
||||
? selectStorageType(store.get().tabStates[currentTab])
|
||||
: StorageType.NONE;
|
||||
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;
|
||||
return (
|
||||
<>
|
||||
<ControlButton
|
||||
popover={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<LaunchConsoleShortcut />
|
||||
<div>Launch Console</div>
|
||||
</div>
|
||||
}
|
||||
disabled={disableButtons}
|
||||
onClick={() => toggleBottomTools(CONSOLE)}
|
||||
active={bottomBlock === CONSOLE && !inspectorMode}
|
||||
label="CONSOLE"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
hasErrors={logRedCount > 0 || showExceptions}
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
|
||||
<ControlButton
|
||||
disabled={disableButtons}
|
||||
onClick={() => toggleBottomTools(NETWORK)}
|
||||
active={bottomBlock === NETWORK && !inspectorMode}
|
||||
label="NETWORK"
|
||||
hasErrors={resourceRedCount > 0}
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
<ControlButton
|
||||
popover={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<LaunchNetworkShortcut />
|
||||
<div>Launch Network</div>
|
||||
</div>
|
||||
}
|
||||
disabled={disableButtons}
|
||||
onClick={() => toggleBottomTools(NETWORK)}
|
||||
active={bottomBlock === NETWORK && !inspectorMode}
|
||||
label="NETWORK"
|
||||
hasErrors={resourceRedCount > 0}
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
|
||||
<ControlButton
|
||||
disabled={disableButtons}
|
||||
onClick={() => toggleBottomTools(PERFORMANCE)}
|
||||
active={bottomBlock === PERFORMANCE && !inspectorMode}
|
||||
label="PERFORMANCE"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
<ControlButton
|
||||
popover={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<LaunchPerformanceShortcut />
|
||||
<div>Launch Performance</div>
|
||||
</div>
|
||||
}
|
||||
disabled={disableButtons}
|
||||
onClick={() => toggleBottomTools(PERFORMANCE)}
|
||||
active={bottomBlock === PERFORMANCE && !inspectorMode}
|
||||
label="PERFORMANCE"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
|
||||
{showGraphql && (
|
||||
<ControlButton
|
||||
disabled={disableButtons}
|
||||
onClick={() => toggleBottomTools(GRAPHQL)}
|
||||
active={bottomBlock === GRAPHQL && !inspectorMode}
|
||||
label="GRAPHQL"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
)}
|
||||
{showGraphql && (
|
||||
<ControlButton
|
||||
disabled={disableButtons}
|
||||
onClick={() => toggleBottomTools(GRAPHQL)}
|
||||
active={bottomBlock === GRAPHQL && !inspectorMode}
|
||||
label="GRAPHQL"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
{showStorage && (
|
||||
<ControlButton
|
||||
disabled={disableButtons}
|
||||
onClick={() => toggleBottomTools(STORAGE)}
|
||||
active={bottomBlock === STORAGE && !inspectorMode}
|
||||
label={getStorageName(storageType)}
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
)}
|
||||
<ControlButton
|
||||
disabled={disableButtons}
|
||||
onClick={() => toggleBottomTools(STACKEVENTS)}
|
||||
active={bottomBlock === STACKEVENTS && !inspectorMode}
|
||||
label="EVENTS"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
hasErrors={stackRedCount > 0}
|
||||
/>
|
||||
{showProfiler && (
|
||||
<ControlButton
|
||||
disabled={disableButtons}
|
||||
onClick={() => toggleBottomTools(PROFILER)}
|
||||
active={bottomBlock === PROFILER && !inspectorMode}
|
||||
label="PROFILER"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
{showStorage && (
|
||||
<ControlButton
|
||||
popover={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<LaunchStateShortcut />
|
||||
<div>Launch State</div>
|
||||
</div>
|
||||
}
|
||||
disabled={disableButtons}
|
||||
onClick={() => toggleBottomTools(STORAGE)}
|
||||
active={bottomBlock === STORAGE && !inspectorMode}
|
||||
label={getStorageName(storageType) as string}
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
)}
|
||||
<ControlButton
|
||||
popover={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<LaunchEventsShortcut />
|
||||
<div>Launch Events</div>
|
||||
</div>
|
||||
}
|
||||
disabled={disableButtons}
|
||||
onClick={() => toggleBottomTools(STACKEVENTS)}
|
||||
active={bottomBlock === STACKEVENTS && !inspectorMode}
|
||||
label="EVENTS"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
hasErrors={stackRedCount > 0}
|
||||
/>
|
||||
{showProfiler && (
|
||||
<ControlButton
|
||||
disabled={disableButtons}
|
||||
onClick={() => toggleBottomTools(PROFILER)}
|
||||
active={bottomBlock === PROFILER && !inspectorMode}
|
||||
label="PROFILER"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const ControlPlayer = observer(Controls);
|
||||
|
||||
|
|
@ -318,6 +348,9 @@ export default connect(
|
|||
session: state.getIn(['sessions', 'current']),
|
||||
totalAssistSessions: state.getIn(['liveSearch', 'total']),
|
||||
skipInterval: state.getIn(['components', 'player', 'skipInterval']),
|
||||
previousSessionId: state.getIn(['sessions', 'previousId']),
|
||||
nextSessionId: state.getIn(['sessions', 'nextId']),
|
||||
siteId: state.getIn(['site', 'siteId']),
|
||||
};
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import { Popover } from 'antd';
|
||||
|
||||
const Key = ({ label }: { label: string }) => <div style={{ minWidth: 52 }} className="whitespace-nowrap font-bold bg-gray-lightest rounded shadow px-2 py-1 text-figmaColors-text-primary text-center">{label}</div>;
|
||||
function Cell({ shortcut, text }: any) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 justify-center text-center rounded">
|
||||
<Key label={shortcut} />
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export const LaunchConsoleShortcut = () => <Key label={"⇧ + C"} />
|
||||
export const LaunchNetworkShortcut = () => <Key label={"⇧ + N"} />
|
||||
export const LaunchPerformanceShortcut = () => <Key label={"⇧ + P"} />
|
||||
export const LaunchStateShortcut = () => <Key label={"⇧ + R"} />
|
||||
export const LaunchEventsShortcut = () => <Key label={"⇧ + E"} />
|
||||
export const PlaySessionInFullscreenShortcut = () => <Key label={"⇧ + F"} />
|
||||
export const PlayPauseSessionShortcut = () => <Key label={"Space"} />
|
||||
export const LaunchXRaShortcut = () => <Key label={"⇧ + X"} />
|
||||
export const LaunchUserActionsShortcut = () => <Key label={"⇧ + A"} />
|
||||
export const LaunchMoreUserInfoShortcut = () => <Key label={"⇧ + I"} />
|
||||
export const LaunchOptionsMenuShortcut = () => <Key label={"⇧ + M"} />
|
||||
export const PlayNextSessionShortcut = () => <Key label={"⇧ + >"} />
|
||||
export const PlayPreviousSessionShortcut = () => <Key label={"⇧ + <"} />
|
||||
export const SkipForwardShortcut = () => <Key label={"→"} />
|
||||
export const SkipBackwardShortcut = () => <Key label={"←"} />
|
||||
export const PlaybackSpeedShortcut = () => <Key label={"↑ / ↓"} />
|
||||
|
||||
function ShortcutGrid() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-flow-row-dense sm:auto-cols-max md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 justify-items-start">
|
||||
<Cell shortcut="⇧ + C" text="Launch Console" />
|
||||
<Cell shortcut="⇧ + N" text="Launch Network" />
|
||||
<Cell shortcut="⇧ + P" text="Launch Performance" />
|
||||
<Cell shortcut="⇧ + R" text="Launch State" />
|
||||
<Cell shortcut="⇧ + E" text="Launch Events" />
|
||||
<Cell shortcut="⇧ + F" text="Play Session in Fullscreen" />
|
||||
<Cell shortcut="Space" text="Play/Pause Session" />
|
||||
<Cell shortcut="⇧ + X" text="Launch X-Ray" />
|
||||
<Cell shortcut="⇧ + A" text="Launch User Actions" />
|
||||
<Cell shortcut="⇧ + I" text="Launch More User Info" />
|
||||
<Cell shortcut="⇧ + M" text="Launch Options Menu" />
|
||||
<Cell shortcut="⇧ + >" text="Play Next Session" />
|
||||
<Cell shortcut="⇧ + <" text="Play Previous Session" />
|
||||
<Cell shortcut="→" text="Skip Forward" />
|
||||
<Cell shortcut="←" text="Skip Backward" />
|
||||
<Cell shortcut="↑" text="Playback Speed Up" />
|
||||
<Cell shortcut="↓" text="Playback Speed Down" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function KeyboardHelp() {
|
||||
return (
|
||||
<Popover
|
||||
title={<div className={'w-full text-center font-semibold'}>Keyboard Shortcuts</div>}
|
||||
content={<ShortcutGrid />}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'py-1 px-2 rounded cursor-help bg-gray-lightest hover:bg-active-blue-border mx-2'
|
||||
}
|
||||
>
|
||||
<Icon name={'keyboard'} size={21} color={'black'} />
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default KeyboardHelp;
|
||||
|
|
@ -1,13 +1,20 @@
|
|||
import * as constants from "constants";
|
||||
import {
|
||||
PlaybackSpeedShortcut,
|
||||
SkipBackwardShortcut,
|
||||
SkipForwardShortcut,
|
||||
SkipIntervalChangeShortcut
|
||||
} from "Components/Session_/Player/Controls/components/KeyboardHelp";
|
||||
import * as constants from 'constants';
|
||||
import React from 'react';
|
||||
import { Icon, Tooltip, Popover } from 'UI';
|
||||
import { Icon, Popover } from 'UI';
|
||||
import { Popover as AntPopover } from 'antd';
|
||||
import cn from 'classnames';
|
||||
import { RealReplayTimeConnected, ReduxTime } from '../Time';
|
||||
// @ts-ignore
|
||||
import styles from '../controls.module.css';
|
||||
import { SkipButton } from 'App/player-ui';
|
||||
import { SPEED_OPTIONS } from 'App/player/player/Player';
|
||||
import PlayingTime from './PlayingTime'
|
||||
import PlayingTime from './PlayingTime';
|
||||
|
||||
interface Props {
|
||||
skip: boolean;
|
||||
|
|
@ -29,8 +36,8 @@ export const TimeMode = {
|
|||
Real: 'real',
|
||||
UserReal: 'user_real',
|
||||
Timestamp: 'current',
|
||||
} as const
|
||||
export type ITimeMode = typeof TimeMode[keyof typeof TimeMode]
|
||||
} as const;
|
||||
export type ITimeMode = (typeof TimeMode)[keyof typeof TimeMode];
|
||||
|
||||
function PlayerControls(props: Props) {
|
||||
const {
|
||||
|
|
@ -49,7 +56,9 @@ function PlayerControls(props: Props) {
|
|||
sessionTz,
|
||||
} = props;
|
||||
const [showTooltip, setShowTooltip] = React.useState(false);
|
||||
const [timeMode, setTimeMode] = React.useState<ITimeMode>(localStorage.getItem('__or_player_time_mode') as ITimeMode);
|
||||
const [timeMode, setTimeMode] = React.useState<ITimeMode>(
|
||||
localStorage.getItem('__or_player_time_mode') as ITimeMode
|
||||
);
|
||||
const speedRef = React.useRef<HTMLButtonElement>(null);
|
||||
const arrowBackRef = React.useRef<HTMLButtonElement>(null);
|
||||
const arrowForwardRef = React.useRef<HTMLButtonElement>(null);
|
||||
|
|
@ -58,7 +67,7 @@ function PlayerControls(props: Props) {
|
|||
const saveTimeMode = (mode: ITimeMode) => {
|
||||
localStorage.setItem('__or_player_time_mode', mode);
|
||||
setTimeMode(mode);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyboard = (e: KeyboardEvent) => {
|
||||
|
|
@ -91,19 +100,27 @@ function PlayerControls(props: Props) {
|
|||
{playButton}
|
||||
<div className="mx-1" />
|
||||
|
||||
|
||||
<button className={cn(styles.speedButton, 'focus:border focus:border-blue')}>
|
||||
<PlayingTime timeMode={timeMode} setTimeMode={saveTimeMode} startedAt={startedAt} sessionTz={sessionTz} />
|
||||
<PlayingTime
|
||||
timeMode={timeMode}
|
||||
setTimeMode={saveTimeMode}
|
||||
startedAt={startedAt}
|
||||
sessionTz={sessionTz}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div className="rounded ml-4 bg-active-blue border border-active-blue-border flex items-stretch">
|
||||
{/* @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`}
|
||||
<AntPopover
|
||||
content={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<SkipBackwardShortcut />
|
||||
<div>{`Rewind ${currentInterval}s`}</div>
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<button ref={arrowBackRef} className="h-full bg-transparent">
|
||||
<button style={{ height: 32, background: 'transparent', border: 0 }} ref={arrowBackRef}>
|
||||
<SkipButton
|
||||
size={18}
|
||||
onClick={backTenSeconds}
|
||||
|
|
@ -111,7 +128,7 @@ function PlayerControls(props: Props) {
|
|||
customClasses={'hover:bg-active-blue-border color-main h-full flex items-center'}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</AntPopover>
|
||||
|
||||
<div className="p-1 border-l border-r bg-active-blue-border border-active-blue-border flex items-center">
|
||||
<Popover
|
||||
|
|
@ -146,27 +163,30 @@ function PlayerControls(props: Props) {
|
|||
)}
|
||||
>
|
||||
<div onClick={toggleTooltip} ref={skipRef} className="cursor-pointer select-none">
|
||||
<Tooltip disabled={showTooltip} title="Set default skip duration">
|
||||
{/* @ts-ignore */}
|
||||
<AntPopover content={<div>Set default skip duration</div>}>
|
||||
{currentInterval}s
|
||||
</Tooltip>
|
||||
</AntPopover>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<Tooltip
|
||||
anchorClassName="h-full hover:border-active-blue-border hover:bg-active-blue-border focus:border focus:border-blue border-borderColor-transparent"
|
||||
title={`Forward ${currentInterval}s →`}
|
||||
<AntPopover
|
||||
content={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<SkipForwardShortcut />
|
||||
<div>{`Forward ${currentInterval}s`}</div>
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<button ref={arrowForwardRef} className="h-full bg-transparent">
|
||||
<button ref={arrowForwardRef} style={{ height: 32, background: 'transparent', border: 0 }}>
|
||||
<SkipButton
|
||||
size={18}
|
||||
onClick={forthTenSeconds}
|
||||
customClasses={'hover:bg-active-blue-border color-main h-full flex items-center'}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</AntPopover>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
|
|
@ -181,9 +201,7 @@ function PlayerControls(props: Props) {
|
|||
distance={20}
|
||||
render={({ close }: any) => (
|
||||
<div className="flex flex-col bg-white border border-borderColor-gray-light-shade text-figmaColors-text-primary rounded">
|
||||
<div className="font-semibold py-2 px-4 w-full text-left">
|
||||
Playback speed
|
||||
</div>
|
||||
<div className="font-semibold py-2 px-4 w-full text-left">Playback speed</div>
|
||||
{Object.keys(SPEED_OPTIONS).map((index: any) => (
|
||||
<div
|
||||
key={SPEED_OPTIONS[index]}
|
||||
|
|
@ -204,7 +222,10 @@ function PlayerControls(props: Props) {
|
|||
)}
|
||||
>
|
||||
<div onClick={toggleTooltip} ref={skipRef} className="cursor-pointer select-none">
|
||||
<Tooltip disabled={showTooltip} title="Playback speed (↑↓)">
|
||||
<AntPopover content={<div className={'flex gap-2 items-center'}>
|
||||
<PlaybackSpeedShortcut />
|
||||
<div>Change playback speed</div>
|
||||
</div>}>
|
||||
<button
|
||||
ref={speedRef}
|
||||
className={cn(styles.speedButton, 'focus:border focus:border-blue')}
|
||||
|
|
@ -212,7 +233,7 @@ function PlayerControls(props: Props) {
|
|||
>
|
||||
<div>{speed + 'x'}</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</AntPopover>
|
||||
</div>
|
||||
</Popover>
|
||||
<div className="mx-2" />
|
||||
|
|
|
|||
|
|
@ -99,11 +99,11 @@ export default connect(
|
|||
(state: any) => ({
|
||||
previousId: state.getIn(['sessions', 'previousId']),
|
||||
nextId: state.getIn(['sessions', 'nextId']),
|
||||
siteId: state.getIn(['site', 'siteId']),
|
||||
currentPage: state.getIn(['search', 'currentPage']) || 1,
|
||||
total: state.getIn(['sessions', 'total']) || 0,
|
||||
sessionIds: state.getIn(['sessions', 'sessionIds']) || [],
|
||||
latestRequestTime: state.getIn(['search', 'latestRequestTime']),
|
||||
siteId: state.getIn(['site', 'siteId']),
|
||||
}),
|
||||
{ setAutoplayValues, fetchAutoplaySessions }
|
||||
)(withRouter(QueueControls));
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ function SubHeader(props) {
|
|||
}
|
||||
/>
|
||||
<ItemMenu
|
||||
useSc
|
||||
items={[
|
||||
{
|
||||
key: 1,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ interface MenuItem {
|
|||
|
||||
interface Props {
|
||||
items: MenuItem[];
|
||||
useSc?: boolean;
|
||||
}
|
||||
|
||||
export default class ItemMenu extends React.PureComponent<Props> {
|
||||
|
|
@ -20,12 +21,19 @@ export default class ItemMenu extends React.PureComponent<Props> {
|
|||
};
|
||||
|
||||
handleEsc = (e: KeyboardEvent) => e.key === 'Escape' && this.state.displayed && this.toggleMenu();
|
||||
handleSc = (e: KeyboardEvent) => e.key === 'M' && e.shiftKey ? this.toggleMenu() : null;
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.handleEsc, false);
|
||||
if (this.props.useSc) {
|
||||
document.addEventListener('keydown', this.handleSc, false)
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.handleEsc, false);
|
||||
if (this.props.useSc) {
|
||||
document.removeEventListener('keydown', this.handleSc, false)
|
||||
}
|
||||
}
|
||||
|
||||
toggleMenu = () => {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { LaunchXRaShortcut } from 'Components/Session_/Player/Controls/components/KeyboardHelp';
|
||||
import React, { useEffect } from 'react';
|
||||
import stl from './xrayButton.module.css';
|
||||
import cn from 'classnames';
|
||||
import { Tooltip } from 'UI';
|
||||
import { Popover } from 'antd'
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
|
||||
interface Props {
|
||||
onClick?: () => void;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
function XRayButton(props: Props) {
|
||||
const { player: Player } = React.useContext(PlayerContext);
|
||||
|
||||
|
|
@ -41,14 +43,21 @@ function XRayButton(props: Props) {
|
|||
></div>
|
||||
)}
|
||||
<div className="relative">
|
||||
<Tooltip title="Get a quick overview on the issues in this session." disabled={isActive}>
|
||||
<Popover
|
||||
content={
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<LaunchXRaShortcut />
|
||||
<div>Get a quick overview on the issues in this session.</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
className={cn(stl.wrapper, { [stl.default]: !isActive, [stl.active]: isActive })}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="z-1">X-RAY</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@ export { default as Integrations_vuejs } from './integrations_vuejs';
|
|||
export { default as Integrations_zustand } from './integrations_zustand';
|
||||
export { default as Journal_code } from './journal_code';
|
||||
export { default as Key } from './key';
|
||||
export { default as Keyboard } from './keyboard';
|
||||
export { default as Layer_group } from './layer_group';
|
||||
export { default as Layers_half } from './layers_half';
|
||||
export { default as Lightbulb_on } from './lightbulb_on';
|
||||
|
|
|
|||
19
frontend/app/components/ui/Icons/keyboard.tsx
Normal file
19
frontend/app/components/ui/Icons/keyboard.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
|
||||
/* Auto-generated, do not edit */
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
fill?: string;
|
||||
}
|
||||
|
||||
function Keyboard(props: Props) {
|
||||
const { size = 14, width = size, height = size, fill = '' } = props;
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M14 5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zM2 4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"/><path d="M13 10.25a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25zm0-2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25zm-5 0A.25.25 0 0 1 8.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 8 8.75zm2 0a.25.25 0 0 1 .25-.25h1.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-1.5a.25.25 0 0 1-.25-.25zm1 2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25zm-5-2A.25.25 0 0 1 6.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 6 8.75zm-2 0A.25.25 0 0 1 4.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 4 8.75zm-2 0A.25.25 0 0 1 2.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 2 8.75zm11-2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25zm-2 0a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25zm-2 0A.25.25 0 0 1 9.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 9 6.75zm-2 0A.25.25 0 0 1 7.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 7 6.75zm-2 0A.25.25 0 0 1 5.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 5 6.75zm-3 0A.25.25 0 0 1 2.25 6h1.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-1.5A.25.25 0 0 1 2 6.75zm0 4a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25zm2 0a.25.25 0 0 1 .25-.25h5.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-5.5a.25.25 0 0 1-.25-.25z"/></svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Keyboard;
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -13,6 +13,36 @@ export const EXCEPTIONS = 9;
|
|||
export const INSPECTOR = 11;
|
||||
export const OVERVIEW = 12;
|
||||
|
||||
export const blocks = {
|
||||
none: NONE,
|
||||
console: CONSOLE,
|
||||
network: NETWORK,
|
||||
stackEvents: STACKEVENTS,
|
||||
storage: STORAGE,
|
||||
profiler: PROFILER,
|
||||
performance: PERFORMANCE,
|
||||
graphql: GRAPHQL,
|
||||
fetch: FETCH,
|
||||
exceptions: EXCEPTIONS,
|
||||
inspector: INSPECTOR,
|
||||
overview: OVERVIEW,
|
||||
} as const
|
||||
|
||||
export const blockValues = [
|
||||
NONE,
|
||||
CONSOLE,
|
||||
NETWORK,
|
||||
STACKEVENTS,
|
||||
STORAGE,
|
||||
PROFILER,
|
||||
PERFORMANCE,
|
||||
GRAPHQL,
|
||||
FETCH,
|
||||
EXCEPTIONS,
|
||||
INSPECTOR,
|
||||
OVERVIEW,
|
||||
] as const
|
||||
|
||||
const TOGGLE_FULLSCREEN = 'player/TOGGLE_FS';
|
||||
const TOGGLE_BOTTOM_BLOCK = 'player/SET_BOTTOM_BLOCK';
|
||||
const HIDE_HINT = 'player/HIDE_HINT';
|
||||
|
|
@ -28,7 +58,7 @@ const initialState = Map({
|
|||
skipInterval: localStorage.getItem(CHANGE_INTERVAL) || 10,
|
||||
});
|
||||
|
||||
const reducer = (state = initialState, action = {}) => {
|
||||
const reducer = (state = initialState, action: any = {}) => {
|
||||
switch (action.type) {
|
||||
case TOGGLE_FULLSCREEN:
|
||||
const { flag } = action
|
||||
|
|
@ -44,7 +74,7 @@ const reducer = (state = initialState, action = {}) => {
|
|||
return state.update('skipInterval', () => skipInterval);
|
||||
case HIDE_HINT:
|
||||
const { name } = action;
|
||||
localStorage.setItem(`${name}HideHint`, true);
|
||||
localStorage.setItem(`${name}HideHint`, 'true');
|
||||
return state
|
||||
.setIn([ "hiddenHints", name ], true)
|
||||
.set('bottomBlock', NONE);
|
||||
|
|
@ -55,7 +85,7 @@ const reducer = (state = initialState, action = {}) => {
|
|||
|
||||
export default reducer;
|
||||
|
||||
export function toggleFullscreen(flag) {
|
||||
export function toggleFullscreen(flag: any) {
|
||||
return {
|
||||
type: TOGGLE_FULLSCREEN,
|
||||
flag,
|
||||
|
|
@ -79,14 +109,14 @@ export function closeBottomBlock() {
|
|||
return toggleBottomBlock();
|
||||
}
|
||||
|
||||
export function changeSkipInterval(skipInterval) {
|
||||
export function changeSkipInterval(skipInterval: number) {
|
||||
return {
|
||||
skipInterval,
|
||||
type: CHANGE_INTERVAL,
|
||||
};
|
||||
}
|
||||
|
||||
export function hideHint(name) {
|
||||
export function hideHint(name: string) {
|
||||
return {
|
||||
name,
|
||||
type: HIDE_HINT,
|
||||
|
|
@ -1,25 +1,32 @@
|
|||
import React from 'react'
|
||||
import { Icon } from 'UI'
|
||||
import cn from 'classnames'
|
||||
import { PlaySessionInFullscreenShortcut } from 'Components/Session_/Player/Controls/components/KeyboardHelp';
|
||||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import { Popover } from 'antd';
|
||||
|
||||
interface IProps {
|
||||
size: number;
|
||||
onClick: () => void;
|
||||
customClasses: string;
|
||||
}
|
||||
|
||||
export function FullScreenButton({ size = 18, onClick, customClasses }: IProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={cn('py-1 px-2 hover-main cursor-pointer bg-gray-lightest', customClasses)}
|
||||
<Popover
|
||||
content={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<PlaySessionInFullscreenShortcut />
|
||||
<div>Play In Fullscreen</div>
|
||||
</div>
|
||||
}
|
||||
placement={"topRight"}
|
||||
>
|
||||
<Icon
|
||||
name="arrows-angle-extend"
|
||||
size={size}
|
||||
color="inherit"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={cn('py-1 px-2 hover-main cursor-pointer bg-gray-lightest', customClasses)}
|
||||
>
|
||||
<Icon name="arrows-angle-extend" size={size} color="inherit" />
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import React from 'react'
|
||||
import { Icon, Tooltip } from "UI";
|
||||
import { PlayPauseSessionShortcut } from 'Components/Session_/Player/Controls/components/KeyboardHelp';
|
||||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import { Popover } from 'antd';
|
||||
|
||||
export enum PlayingState {
|
||||
Playing,
|
||||
Paused,
|
||||
Completed
|
||||
Completed,
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
|
|
@ -16,7 +18,7 @@ interface IProps {
|
|||
const Values = {
|
||||
[PlayingState.Playing]: {
|
||||
icon: 'pause-fill' as const,
|
||||
label: 'Pause'
|
||||
label: 'Pause',
|
||||
},
|
||||
[PlayingState.Completed]: {
|
||||
icon: 'arrow-clockwise' as const,
|
||||
|
|
@ -24,21 +26,28 @@ const Values = {
|
|||
},
|
||||
[PlayingState.Paused]: {
|
||||
icon: 'play-fill-new' as const,
|
||||
label: 'Play'
|
||||
}
|
||||
}
|
||||
label: 'Play',
|
||||
},
|
||||
};
|
||||
|
||||
export function PlayButton({ togglePlay, iconSize, state }: IProps) {
|
||||
const { icon, label } = Values[state];
|
||||
|
||||
return (
|
||||
<Tooltip title={label} className="mr-4">
|
||||
<Popover
|
||||
content={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<PlayPauseSessionShortcut />
|
||||
<div>{label}</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
onClick={togglePlay}
|
||||
className="hover-main color-main cursor-pointer rounded hover:bg-gray-light-shade"
|
||||
>
|
||||
<Icon name={icon} size={iconSize} color="inherit" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
4
frontend/app/svg/icons/keyboard.svg
Normal file
4
frontend/app/svg/icons/keyboard.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="M14 5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zM2 4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"/>
|
||||
<path d="M13 10.25a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25zm0-2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25zm-5 0A.25.25 0 0 1 8.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 8 8.75zm2 0a.25.25 0 0 1 .25-.25h1.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-1.5a.25.25 0 0 1-.25-.25zm1 2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25zm-5-2A.25.25 0 0 1 6.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 6 8.75zm-2 0A.25.25 0 0 1 4.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 4 8.75zm-2 0A.25.25 0 0 1 2.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 2 8.75zm11-2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25zm-2 0a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25zm-2 0A.25.25 0 0 1 9.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 9 6.75zm-2 0A.25.25 0 0 1 7.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 7 6.75zm-2 0A.25.25 0 0 1 5.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 5 6.75zm-3 0A.25.25 0 0 1 2.25 6h1.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-1.5A.25.25 0 0 1 2 6.75zm0 4a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25zm2 0a.25.25 0 0 1 .25-.25h5.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-5.5a.25.25 0 0 1-.25-.25z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
Loading…
Add table
Reference in a new issue