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:
Delirium 2024-03-18 11:09:49 +01:00 committed by GitHub
parent 902ec87d7a
commit c0f4a99545
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 584 additions and 2473 deletions

View file

@ -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>
);
}

View file

@ -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);

View file

@ -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>
//
//

View file

@ -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;
}
}
}

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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;
}

View file

@ -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>
</>
);
})

View file

@ -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);

View file

@ -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} />);

View file

@ -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);

View file

@ -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)

View file

@ -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);

View file

@ -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>
);
};

View file

@ -1,8 +0,0 @@
.wrapper {
display: flex;
flex-flow: column;
height: 40px;
justify-content: space-between;
font-size: 12px;
color: $gray-medium;
}

View file

@ -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);

View file

@ -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>
);
}

View file

@ -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;
}

View file

@ -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>
);
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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);

View file

@ -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);

View file

@ -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>
);
}

View file

@ -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;
}
}

View file

@ -1,4 +0,0 @@
.time {
padding: 0 12px;
color: $gray-medium;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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>
);
}

View file

@ -1,10 +0,0 @@
.line {
padding: 7px 0 7px 15px;
}
.message {
font-size: 13px;
&::-webkit-scrollbar {
height: 2px;
}
}

View file

@ -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>
);
}

View file

@ -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>
}
</>
);
}
}

View file

@ -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);

View file

@ -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;
}

View file

@ -61,6 +61,7 @@ function SubHeader(props: any) {
}
/>
<ItemMenu
useSc
items={menuItems}
/>

View file

@ -181,7 +181,7 @@ function SubHeader(props: any) {
>
<SummaryButton onClick={showSummary} />
<NotePopup />
<ItemMenu items={additionalMenu} />
<ItemMenu items={additionalMenu} useSc />
{uxtestingStore.isUxt() ? (
<Switch
checkedChildren={'DevTools'}

View file

@ -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">&#183;</span>
<Popover
render={() => (
open={showMore ? true : undefined}
content={() => (
<div className="text-left bg-white rounded">
<SessionInfoItem
comp={<CountryFlag country={userCountry} height={11} />}

View file

@ -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>
);
}

View file

@ -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() && (

View file

@ -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}

View file

@ -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;

View file

@ -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';

View file

@ -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']),
};
},
{

View file

@ -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;

View file

@ -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" />

View file

@ -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));

View file

@ -142,6 +142,7 @@ function SubHeader(props) {
}
/>
<ItemMenu
useSc
items={[
{
key: 1,

View file

@ -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 = () => {

View file

@ -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>
</>
);

View file

@ -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';

View 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

View file

@ -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,

View file

@ -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>
);
}

View file

@ -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>
);
}

View 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