feat(ui) - overview - toggle button and other optimizations

This commit is contained in:
Shekar Siri 2022-08-16 14:49:20 +02:00
parent 193e83f0d2
commit f7c0cf11fb
19 changed files with 1151 additions and 989 deletions

View file

@ -122,6 +122,7 @@ const Header = (props) => {
</div>
<ul>
<li><button onClick={ onLogoutClick }>{ 'Account' }</button></li>
<li><button onClick={ onLogoutClick }>{ 'Logout' }</button></li>
</ul>
</div>

View file

@ -49,7 +49,7 @@ function LivePlayer ({
}, [])
const TABS = {
EVENTS: 'Events',
EVENTS: 'User Actions',
HEATMAPS: 'Click Map',
}
const [activeTab, setActiveTab] = useState('');

View file

@ -14,7 +14,7 @@ import styles from '../Session_/session.module.css';
import { countDaysFrom } from 'App/date';
const TABS = {
EVENTS: 'Events',
EVENTS: 'User Actions',
HEATMAPS: 'Click Map',
};

View file

@ -197,7 +197,7 @@ export default class EventsBlock extends React.PureComponent {
setActiveTab={setActiveTab}
value={query}
header={
<div className="text-xl">User Events <span className="color-gray-medium">{ events.size }</span></div>
<div className="text-xl">User Actions <span className="color-gray-medium">{ events.size }</span></div>
}
/>
</div>

View file

@ -1,6 +1,6 @@
import { connectPlayer } from 'App/player';
import { toggleBottomBlock } from 'Duck/components/player';
import React from 'react';
import React, { useEffect } from 'react';
import BottomBlock from '../BottomBlock';
import EventRow from './components/EventRow';
import { TYPES } from 'Types/session/event';
@ -10,7 +10,7 @@ import FeatureSelection from './components/FeatureSelection/FeatureSelection';
import TimelinePointer from './components/TimelinePointer';
import VerticalPointerLine from './components/VerticalPointerLine';
import cn from 'classnames';
import VerticalLine from './components/VerticalLine';
// import VerticalLine from './components/VerticalLine';
import OverviewPanelContainer from './components/OverviewPanelContainer';
interface Props {
@ -20,52 +20,67 @@ interface Props {
toggleBottomBlock: any;
stackEventList: any[];
issuesList: any[];
performanceChartData: any;
endTime: number;
}
function OverviewPanel(props: Props) {
const { resourceList, exceptionsList, eventsList, stackEventList, issuesList } = props;
const clickRageList = React.useMemo(() => {
return eventsList.filter((item: any) => item.type === TYPES.CLICKRAGE);
}, [eventsList]);
const [dataLoaded, setDataLoaded] = React.useState(false);
const [selectedFeatures, setSelectedFeatures] = React.useState(['PERFORMANCE', 'ERRORS', 'EVENTS']);
const resources: any = React.useMemo(() => {
const { resourceList, exceptionsList, eventsList, stackEventList, issuesList, performanceChartData } = props;
return {
NETWORK: resourceList,
ERRORS: exceptionsList,
EVENTS: stackEventList,
CLICKRAGE: clickRageList,
PERFORMANCE: issuesList,
CLICKRAGE: eventsList.filter((item: any) => item.type === TYPES.CLICKRAGE),
PERFORMANCE: performanceChartData,
};
}, [resourceList, exceptionsList, stackEventList, clickRageList, issuesList]);
}, [dataLoaded]);
useEffect(() => {
if (dataLoaded) {
return;
}
if (props.resourceList.length > 0) {
setDataLoaded(true);
}
}, [props.resourceList]);
return (
<BottomBlock style={{ height: '260px' }}>
<BottomBlock.Header>
<span className="font-semibold color-gray-medium mr-4">Overview</span>
<div className="flex items-center h-20">
<FeatureSelection list={selectedFeatures} updateList={setSelectedFeatures} />
</div>
</BottomBlock.Header>
<BottomBlock.Content>
<OverviewPanelContainer>
<TimelineScale />
<div style={{ width: '100%', height: '200px' }} className="transition relative">
<VerticalPointerLine />
{selectedFeatures.map((feature: any, index: number) => (
<div className={cn('', { 'bg-white border-t border-b': index % 2 })}>
<EventRow
key={feature}
title={feature}
list={resources[feature]}
renderElement={(pointer: any) => <TimelinePointer pointer={pointer} type={feature} />}
/>
dataLoaded && (
<Wrapper {...props}>
<BottomBlock style={{ height: '250px' }}>
<BottomBlock.Header>
<span className="font-semibold color-gray-medium mr-4">X-RAY</span>
<div className="flex items-center h-20">
<FeatureSelection list={selectedFeatures} updateList={setSelectedFeatures} />
</div>
</BottomBlock.Header>
<BottomBlock.Content>
<OverviewPanelContainer endTime={props.endTime}>
<TimelineScale endTime={props.endTime} />
<div style={{ width: '100%', height: '200px' }} className="transition relative">
<VerticalPointerLine />
{selectedFeatures.map((feature: any, index: number) => (
<div className={cn('border-b', { 'bg-white': index % 2 })}>
<EventRow
isGraph={feature === 'PERFORMANCE'}
key={feature}
title={feature}
list={resources[feature]}
renderElement={(pointer: any) => <TimelinePointer pointer={pointer} type={feature} />}
endTime={props.endTime}
/>
</div>
))}
</div>
))}
</div>
</OverviewPanelContainer>
</BottomBlock.Content>
</BottomBlock>
</OverviewPanelContainer>
</BottomBlock.Content>
</BottomBlock>
</Wrapper>
)
);
}
@ -82,5 +97,12 @@ export default connect(
exceptionsList: state.exceptionsList,
eventsList: state.eventList,
stackEventList: state.stackList,
performanceChartData: state.performanceChartData,
endTime: state.endTime,
// endTime: 30000000,
}))(OverviewPanel)
);
const Wrapper = React.memo((props: any) => {
return <div>{props.children}</div>;
});

View file

@ -1,43 +1,48 @@
import React from 'react';
import cn from 'classnames'
import cn from 'classnames';
import { getTimelinePosition } from 'App/utils';
import { connectPlayer } from 'App/player';
import PerformanceGraph from '../PerformanceGraph';
interface Props {
list?: any[];
title: string;
className?: string;
endTime?: number;
renderElement?: (item: any) => React.ReactNode;
isGraph?: boolean;
}
const EventRow = React.memo((props: Props) => {
const { title, className, list = [], endTime = 0 } = props;
const { title, className, list = [], endTime = 0, isGraph = false } = props;
const scale = 100 / endTime;
const _list = React.useMemo(() => {
return list.map((item: any, _index: number) => {
return {
...item.toJS(),
left: getTimelinePosition(item.time, scale),
}
})
}, [list]);
const _list =
!isGraph &&
React.useMemo(() => {
return list.map((item: any, _index: number) => {
return {
...item.toJS(),
left: getTimelinePosition(item.time, scale),
};
});
}, [list]);
return (
<div className={cn('w-full flex flex-col py-2', className)} style={{ height: '66px'}}>
<div className="uppercase color-gray-medium ml-4">{title}</div>
<div className="relative w-full py-3">
{_list.map((item: any, index: number) => {
return (
<div key={index} className="absolute" style={{ left: item.left + '%'}} >
{props.renderElement ? props.renderElement(item) : null}
</div>
)
}
)}
<div className={cn('w-full flex flex-col py-2', className)} style={{ height: '60px' }}>
<div className="uppercase color-gray-medium ml-4 text-sm">{title}</div>
<div className="relative w-full">
{isGraph ? (
<PerformanceGraph list={list} />
) : (
_list.map((item: any, index: number) => {
return (
<div key={index} className="absolute" style={{ left: item.left + '%' }}>
{props.renderElement ? props.renderElement(item) : null}
</div>
);
})
)}
</div>
</div>
);
});
export default connectPlayer((state: any) => ({
endTime: state.endTime,
}))(EventRow);
export default EventRow;

View file

@ -7,29 +7,31 @@ interface Props {
endTime: number;
}
function OverviewPanelContainer(props: Props) {
const OverviewPanelContainer = React.memo((props: Props) => {
const { endTime } = props;
const [mouseX, setMouseX] = React.useState(0);
const [mouseIn, setMouseIn] = React.useState(false);
const onClickTrack = (e: any) => {
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
Controls.jump(time);
if (time) {
Controls.jump(time);
}
};
const onMouseMoveCapture = (e: any) => {
if (!mouseIn) {
return;
}
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
setMouseX(p * 100);
};
// const onMouseMoveCapture = (e: any) => {
// if (!mouseIn) {
// return;
// }
// const p = e.nativeEvent.offsetX / e.target.offsetWidth;
// setMouseX(p * 100);
// };
return (
<div
className="overflow-x-auto overflow-y-hidden bg-gray-lightest"
onClick={onClickTrack}
onMouseMoveCapture={onMouseMoveCapture}
// onMouseMoveCapture={onMouseMoveCapture}
// onMouseOver={() => setMouseIn(true)}
// onMouseOut={() => setMouseIn(false)}
>
@ -37,8 +39,10 @@ function OverviewPanelContainer(props: Props) {
<div className="">{props.children}</div>
</div>
);
}
});
export default connectPlayer((state: any) => ({
endTime: state.endTime,
}))(OverviewPanelContainer);
export default OverviewPanelContainer;
// export default connectPlayer((state: any) => ({
// endTime: state.endTime,
// }))(OverviewPanelContainer);

View file

@ -0,0 +1,83 @@
import React from 'react';
import { connectPlayer } from 'App/player';
import { AreaChart, Area, Tooltip, ResponsiveContainer } from 'recharts';
interface Props {
list: any;
}
const PerformanceGraph = React.memo((props: Props) => {
const { list } = props;
const finalValues = React.useMemo(() => {
const cpuMax = list.reduce((acc: number, item: any) => {
return Math.max(acc, item.cpu);
}, 0);
const cpuMin = list.reduce((acc: number, item: any) => {
return Math.min(acc, item.cpu);
}, Infinity);
const memoryMin = list.reduce((acc: number, item: any) => {
return Math.min(acc, item.usedHeap);
}, Infinity);
const memoryMax = list.reduce((acc: number, item: any) => {
return Math.max(acc, item.usedHeap);
}, 0);
const convertToPercentage = (val: number, max: number, min: number) => {
return ((val - min) / (max - min)) * 100;
};
const cpuValues = list.map((item: any) => convertToPercentage(item.cpu, cpuMax, cpuMin));
const memoryValues = list.map((item: any) => convertToPercentage(item.usedHeap, memoryMax, memoryMin));
const mergeArraysWithMaxNumber = (arr1: any[], arr2: any[]) => {
const maxLength = Math.max(arr1.length, arr2.length);
const result = [];
for (let i = 0; i < maxLength; i++) {
const num = Math.round(Math.max(arr1[i] || 0, arr2[i] || 0));
result.push(num > 60 ? num : 1);
}
return result;
};
const finalValues = mergeArraysWithMaxNumber(cpuValues, memoryValues);
return finalValues;
}, []);
const data = list.map((item: any, index: number) => {
return {
time: item.time,
cpu: finalValues[index],
};
});
return (
<ResponsiveContainer height={35}>
<AreaChart
data={data}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<linearGradient id="cpuGradientTimeline" x1="0" y1="0" x2="0" y2="1">
<stop offset="30%" stopColor="#CC0000" stopOpacity={0.5} />
<stop offset="95%" stopColor="#3EAAAF" stopOpacity={0.8} />
</linearGradient>
</defs>
{/* <Tooltip filterNull={false} /> */}
<Area
dataKey="cpu"
baseValue={5}
type="monotone"
stroke="none"
activeDot={false}
fill="url(#cpuGradientTimeline)"
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
);
});
export default PerformanceGraph;

View file

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

View file

@ -45,7 +45,7 @@ const TimelinePointer = React.memo((props: Props) => {
position="top"
>
<div onClick={createEventClickHandler(item, NETWORK)} className="cursor-pointer">
<div className="h-2 w-2 rounded-full bg-red" />
<div className="h-3 w-3 rounded-full bg-red" />
</div>
</Popup>
);
@ -92,7 +92,7 @@ const TimelinePointer = React.memo((props: Props) => {
<Popup
content={
<div className="">
<b>{item.name}</b>
<b>{item.type}</b>
</div>
}
delay={0}

View file

@ -1,14 +1,60 @@
import React from 'react';
import { connectPlayer } from 'App/player';
import { millisToMinutesAndSeconds } from 'App/utils';
interface Props {
endTime: number;
}
function TimelineScale(props: Props) {
return (
<div className="h-6 bg-gray-darkest w-full">
</div>
);
const { endTime } = props;
const scaleRef = React.useRef<HTMLDivElement>(null);
const gap = 60;
const drawScale = (container: any) => {
const width = container.offsetWidth;
const part = Math.round(width / gap);
container.replaceChildren();
for (var i = 0; i < part; i++) {
const txt = millisToMinutesAndSeconds(i * (endTime / part));
const el = document.createElement('div');
// el.style.height = '10px';
// el.style.width = '1px';
// el.style.backgroundColor = '#ccc';
el.style.position = 'absolute';
el.style.left = `${i * gap}px`;
el.style.paddingTop = '1px';
el.style.opacity = '0.8';
el.innerHTML = txt + '';
el.style.fontSize = '12px';
el.style.color = 'white';
container.appendChild(el);
}
};
React.useEffect(() => {
if (!scaleRef.current) {
return;
}
drawScale(scaleRef.current);
// const resize = () => drawScale(scaleRef.current);
// window.addEventListener('resize', resize);
// return () => {
// window.removeEventListener('resize', resize);
// };
}, [scaleRef]);
return (
<div className="h-6 bg-gray-darkest w-full" ref={scaleRef}>
{/* <div ref={scaleRef} className="w-full h-10 bg-gray-300 relative"></div> */}
</div>
);
}
export default TimelineScale;
export default TimelineScale;
// export default connectPlayer((state: any) => ({
// endTime: state.endTime,
// }))(TimelineScale);

View file

@ -1,344 +1,342 @@
import React from 'react';
import cn from 'classnames';
import { connect } from 'react-redux';
import {
connectPlayer,
STORAGE_TYPES,
selectStorageType,
selectStorageListNow,
} from 'Player/store';
import { connectPlayer, STORAGE_TYPES, selectStorageType, selectStorageListNow } from 'Player/store';
import LiveTag from 'Shared/LiveTag';
import { session as sessionRoute, withSiteId } from 'App/routes';
import {
toggleTimetravel,
jumpToLive,
} from 'Player';
import { toggleTimetravel, jumpToLive } from 'Player';
import { Icon } from 'UI';
import { Icon, Button } from 'UI';
import { toggleInspectorMode } from 'Player';
import {
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
OVERVIEW,
CONSOLE,
NETWORK,
STACKEVENTS,
STORAGE,
PROFILER,
PERFORMANCE,
GRAPHQL,
FETCH,
EXCEPTIONS,
INSPECTOR,
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
OVERVIEW,
CONSOLE,
NETWORK,
STACKEVENTS,
STORAGE,
PROFILER,
PERFORMANCE,
GRAPHQL,
FETCH,
EXCEPTIONS,
INSPECTOR,
} from 'Duck/components/player';
import { ReduxTime, AssistDuration } from './Time';
import Timeline from './Timeline';
import ControlButton from './ControlButton';
import PlayerControls from './components/PlayerControls'
import PlayerControls from './components/PlayerControls';
import styles from './controls.module.css';
import { Tooltip } from 'react-tippy';
import XRayButton from 'Shared/XRayButton';
function getStorageIconName(type) {
switch(type) {
case STORAGE_TYPES.REDUX:
return "vendors/redux";
case STORAGE_TYPES.MOBX:
return "vendors/mobx"
case STORAGE_TYPES.VUEX:
return "vendors/vuex";
case STORAGE_TYPES.NGRX:
return "vendors/ngrx";
case STORAGE_TYPES.NONE:
return "store"
}
switch (type) {
case STORAGE_TYPES.REDUX:
return 'vendors/redux';
case STORAGE_TYPES.MOBX:
return 'vendors/mobx';
case STORAGE_TYPES.VUEX:
return 'vendors/vuex';
case STORAGE_TYPES.NGRX:
return 'vendors/ngrx';
case STORAGE_TYPES.NONE:
return 'store';
}
}
function getStorageName(type) {
switch(type) {
case STORAGE_TYPES.REDUX:
return "REDUX";
case STORAGE_TYPES.MOBX:
return "MOBX";
case STORAGE_TYPES.VUEX:
return "VUEX";
case STORAGE_TYPES.NGRX:
return "NGRX";
case STORAGE_TYPES.NONE:
return "STATE";
}
switch (type) {
case STORAGE_TYPES.REDUX:
return 'REDUX';
case STORAGE_TYPES.MOBX:
return 'MOBX';
case STORAGE_TYPES.VUEX:
return 'VUEX';
case STORAGE_TYPES.NGRX:
return 'NGRX';
case STORAGE_TYPES.NONE:
return 'STATE';
}
}
@connectPlayer(state => ({
time: state.time,
endTime: state.endTime,
live: state.live,
livePlay: state.livePlay,
playing: state.playing,
completed: state.completed,
skip: state.skip,
skipToIssue: state.skipToIssue,
speed: state.speed,
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode || state.markedTargets,
inspectorMode: state.inspectorMode,
fullscreenDisabled: state.messagesLoading,
logCount: state.logListNow.length,
logRedCount: state.logRedCountNow,
resourceRedCount: state.resourceRedCountNow,
fetchRedCount: state.fetchRedCountNow,
showStack: state.stackList.length > 0,
stackCount: state.stackListNow.length,
stackRedCount: state.stackRedCountNow,
profilesCount: state.profilesListNow.length,
storageCount: selectStorageListNow(state).length,
storageType: selectStorageType(state),
showStorage: selectStorageType(state) !== STORAGE_TYPES.NONE,
showProfiler: state.profilesList.length > 0,
showGraphql: state.graphqlList.length > 0,
showFetch: state.fetchCount > 0,
fetchCount: state.fetchCountNow,
graphqlCount: state.graphqlListNow.length,
exceptionsCount: state.exceptionsListNow.length,
showExceptions: state.exceptionsList.length > 0,
showLongtasks: state.longtasksList.length > 0,
liveTimeTravel: state.liveTimeTravel,
@connectPlayer((state) => ({
time: state.time,
endTime: state.endTime,
live: state.live,
livePlay: state.livePlay,
playing: state.playing,
completed: state.completed,
skip: state.skip,
skipToIssue: state.skipToIssue,
speed: state.speed,
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode || state.markedTargets,
inspectorMode: state.inspectorMode,
fullscreenDisabled: state.messagesLoading,
logCount: state.logListNow.length,
logRedCount: state.logRedCountNow,
resourceRedCount: state.resourceRedCountNow,
fetchRedCount: state.fetchRedCountNow,
showStack: state.stackList.length > 0,
stackCount: state.stackListNow.length,
stackRedCount: state.stackRedCountNow,
profilesCount: state.profilesListNow.length,
storageCount: selectStorageListNow(state).length,
storageType: selectStorageType(state),
showStorage: selectStorageType(state) !== STORAGE_TYPES.NONE,
showProfiler: state.profilesList.length > 0,
showGraphql: state.graphqlList.length > 0,
showFetch: state.fetchCount > 0,
fetchCount: state.fetchCountNow,
graphqlCount: state.graphqlListNow.length,
exceptionsCount: state.exceptionsListNow.length,
showExceptions: state.exceptionsList.length > 0,
showLongtasks: state.longtasksList.length > 0,
liveTimeTravel: state.liveTimeTravel,
}))
@connect((state, props) => {
const permissions = state.getIn([ 'user', 'account', 'permissions' ]) || [];
const isEnterprise = state.getIn([ 'user', 'account', 'edition' ]) === 'ee';
return {
disabled: props.disabled || (isEnterprise && !permissions.includes('DEV_TOOLS')),
fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]),
bottomBlock: state.getIn([ 'components', 'player', 'bottomBlock' ]),
showStorage: props.showStorage || !state.getIn(['components', 'player', 'hiddenHints', 'storage']),
showStack: props.showStack || !state.getIn(['components', 'player', 'hiddenHints', 'stack']),
closedLive: !!state.getIn([ 'sessions', 'errors' ]) || !state.getIn([ 'sessions', 'current', 'live' ]),
}
}, {
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
})
@connect(
(state, props) => {
const permissions = state.getIn(['user', 'account', 'permissions']) || [];
const isEnterprise = state.getIn(['user', 'account', 'edition']) === 'ee';
return {
disabled: props.disabled || (isEnterprise && !permissions.includes('DEV_TOOLS')),
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
bottomBlock: state.getIn(['components', 'player', 'bottomBlock']),
showStorage: props.showStorage || !state.getIn(['components', 'player', 'hiddenHints', 'storage']),
showStack: props.showStack || !state.getIn(['components', 'player', 'hiddenHints', 'stack']),
closedLive: !!state.getIn(['sessions', 'errors']) || !state.getIn(['sessions', 'current', 'live']),
};
},
{
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
}
)
export default class Controls extends React.Component {
componentDidMount() {
document.addEventListener('keydown', this.onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onKeyDown);
//this.props.toggleInspectorMode(false);
}
shouldComponentUpdate(nextProps) {
if (
nextProps.fullscreen !== this.props.fullscreen ||
nextProps.bottomBlock !== this.props.bottomBlock ||
nextProps.live !== this.props.live ||
nextProps.livePlay !== this.props.livePlay ||
nextProps.playing !== this.props.playing ||
nextProps.completed !== this.props.completed ||
nextProps.skip !== this.props.skip ||
nextProps.skipToIssue !== this.props.skipToIssue ||
nextProps.speed !== this.props.speed ||
nextProps.disabled !== this.props.disabled ||
nextProps.fullscreenDisabled !== this.props.fullscreenDisabled ||
// nextProps.inspectorMode !== this.props.inspectorMode ||
nextProps.logCount !== this.props.logCount ||
nextProps.logRedCount !== this.props.logRedCount ||
nextProps.resourceRedCount !== this.props.resourceRedCount ||
nextProps.fetchRedCount !== this.props.fetchRedCount ||
nextProps.showStack !== this.props.showStack ||
nextProps.stackCount !== this.props.stackCount ||
nextProps.stackRedCount !== this.props.stackRedCount ||
nextProps.profilesCount !== this.props.profilesCount ||
nextProps.storageCount !== this.props.storageCount ||
nextProps.storageType !== this.props.storageType ||
nextProps.showStorage !== this.props.showStorage ||
nextProps.showProfiler !== this.props.showProfiler ||
nextProps.showGraphql !== this.props.showGraphql ||
nextProps.showFetch !== this.props.showFetch ||
nextProps.fetchCount !== this.props.fetchCount ||
nextProps.graphqlCount !== this.props.graphqlCount ||
nextProps.showExceptions !== this.props.showExceptions ||
nextProps.exceptionsCount !== this.props.exceptionsCount ||
nextProps.showLongtasks !== this.props.showLongtasks ||
nextProps.liveTimeTravel !== this.props.liveTimeTravel
) return true;
return false;
}
onKeyDown = (e) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
componentDidMount() {
document.addEventListener('keydown', this.onKeyDown);
}
if (this.props.inspectorMode) {
if (e.key === 'Esc' || e.key === 'Escape') {
toggleInspectorMode(false);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onKeyDown);
//this.props.toggleInspectorMode(false);
}
shouldComponentUpdate(nextProps) {
if (
nextProps.fullscreen !== this.props.fullscreen ||
nextProps.bottomBlock !== this.props.bottomBlock ||
nextProps.live !== this.props.live ||
nextProps.livePlay !== this.props.livePlay ||
nextProps.playing !== this.props.playing ||
nextProps.completed !== this.props.completed ||
nextProps.skip !== this.props.skip ||
nextProps.skipToIssue !== this.props.skipToIssue ||
nextProps.speed !== this.props.speed ||
nextProps.disabled !== this.props.disabled ||
nextProps.fullscreenDisabled !== this.props.fullscreenDisabled ||
// nextProps.inspectorMode !== this.props.inspectorMode ||
nextProps.logCount !== this.props.logCount ||
nextProps.logRedCount !== this.props.logRedCount ||
nextProps.resourceRedCount !== this.props.resourceRedCount ||
nextProps.fetchRedCount !== this.props.fetchRedCount ||
nextProps.showStack !== this.props.showStack ||
nextProps.stackCount !== this.props.stackCount ||
nextProps.stackRedCount !== this.props.stackRedCount ||
nextProps.profilesCount !== this.props.profilesCount ||
nextProps.storageCount !== this.props.storageCount ||
nextProps.storageType !== this.props.storageType ||
nextProps.showStorage !== this.props.showStorage ||
nextProps.showProfiler !== this.props.showProfiler ||
nextProps.showGraphql !== this.props.showGraphql ||
nextProps.showFetch !== this.props.showFetch ||
nextProps.fetchCount !== this.props.fetchCount ||
nextProps.graphqlCount !== this.props.graphqlCount ||
nextProps.showExceptions !== this.props.showExceptions ||
nextProps.exceptionsCount !== this.props.exceptionsCount ||
nextProps.showLongtasks !== this.props.showLongtasks ||
nextProps.liveTimeTravel !== this.props.liveTimeTravel
)
return true;
return false;
}
onKeyDown = (e) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
if (this.props.inspectorMode) {
if (e.key === 'Esc' || e.key === 'Escape') {
toggleInspectorMode(false);
}
}
// if (e.key === ' ') {
// document.activeElement.blur();
// this.props.togglePlay();
// }
if (e.key === 'Esc' || e.key === 'Escape') {
this.props.fullscreenOff();
}
if (e.key === 'ArrowRight') {
this.forthTenSeconds();
}
if (e.key === 'ArrowLeft') {
this.backTenSeconds();
}
if (e.key === 'ArrowDown') {
this.props.speedDown();
}
if (e.key === 'ArrowUp') {
this.props.speedUp();
}
};
// if (e.key === ' ') {
// document.activeElement.blur();
// this.props.togglePlay();
// }
if (e.key === 'Esc' || e.key === 'Escape') {
this.props.fullscreenOff();
}
if (e.key === "ArrowRight") {
this.forthTenSeconds();
}
if (e.key === "ArrowLeft") {
this.backTenSeconds();
}
if (e.key === "ArrowDown") {
this.props.speedDown();
}
if (e.key === "ArrowUp") {
this.props.speedUp();
}
}
forthTenSeconds = () => {
const { time, endTime, jump } = this.props;
jump(Math.min(endTime, time + 1e4))
}
forthTenSeconds = () => {
const { time, endTime, jump } = this.props;
jump(Math.min(endTime, time + 1e4));
};
backTenSeconds = () => { //shouldComponentUpdate
const { time, jump } = this.props;
jump(Math.max(0, time - 1e4));
}
backTenSeconds = () => {
//shouldComponentUpdate
const { time, jump } = this.props;
jump(Math.max(0, time - 1e4));
};
goLive =() => this.props.jump(this.props.endTime)
goLive = () => this.props.jump(this.props.endTime);
renderPlayBtn = () => {
const { completed, playing } = this.props;
let label;
let icon;
if (completed) {
icon = 'arrow-clockwise';
label = 'Replay this session'
} else if (playing) {
icon = 'pause-fill';
label = 'Pause';
} else {
icon = 'play-fill-new';
label = 'Pause';
label = 'Play'
}
renderPlayBtn = () => {
const { completed, playing } = this.props;
let label;
let icon;
if (completed) {
icon = 'arrow-clockwise';
label = 'Replay this session';
} else if (playing) {
icon = 'pause-fill';
label = 'Pause';
} else {
icon = 'play-fill-new';
label = 'Pause';
label = 'Play';
}
return (
<Tooltip
delay={0}
position="top"
title={label}
interactive
hideOnClick="persistent"
className="mr-4"
>
<div
onClick={this.props.togglePlay}
className="hover-main color-main cursor-pointer rounded hover:bg-gray-light-shade"
>
<Icon name={icon} size="36" color="inherit" />
</div>
</Tooltip>
)
}
controlIcon = (icon, size, action, isBackwards, additionalClasses) =>
<div
onClick={ action }
className={cn("py-1 px-2 hover-main cursor-pointer", additionalClasses)}
style={{ transform: isBackwards ? 'rotate(180deg)' : '' }}
>
<Icon name={icon} size={size} color="inherit" />
</div>
render() {
const {
bottomBlock,
toggleBottomBlock,
live,
livePlay,
skip,
speed,
disabled,
logCount,
logRedCount,
resourceRedCount,
fetchRedCount,
showStack,
stackCount,
stackRedCount,
profilesCount,
storageCount,
showStorage,
storageType,
showProfiler,
showGraphql,
showFetch,
fetchCount,
graphqlCount,
exceptionsCount,
showExceptions,
fullscreen,
inspectorMode,
closedLive,
toggleSpeed,
toggleSkip,
liveTimeTravel,
} = this.props;
const toggleBottomTools = (blockName) => {
if (blockName === INSPECTOR) {
toggleInspectorMode();
bottomBlock && toggleBottomBlock();
} else {
toggleInspectorMode(false);
toggleBottomBlock(blockName);
}
}
return (
<div className={ styles.controls }>
{ !live || liveTimeTravel ? <Timeline jump={ this.props.jump } liveTimeTravel={liveTimeTravel} pause={this.props.pause} togglePlay={this.props.togglePlay} /> : null}
{ !fullscreen &&
<div className={ cn(styles.buttons, {'!px-5 !pt-0' : live}) } data-is-live={ live }>
<div>
{!live && (
<PlayerControls
live={live}
skip={skip}
speed={speed}
disabled={disabled}
backTenSeconds={this.backTenSeconds}
forthTenSeconds={this.forthTenSeconds}
toggleSpeed={toggleSpeed}
toggleSkip={toggleSkip}
playButton={this.renderPlayBtn()}
controlIcon={this.controlIcon}
/>
)}
{ live && !closedLive && (
<div className={ styles.buttonsLeft }>
<LiveTag isLive={livePlay} onClick={() => livePlay ? null : jumpToLive()} />
<div className="font-semibold px-2"><AssistDuration isLivePlay={livePlay} /></div>
{!liveTimeTravel && (
<div onClick={toggleTimetravel} className="p-2 ml-2 rounded hover:bg-teal-light bg-gray-lightest cursor-pointer">
See Past Activity
</div>
)}
return (
<Tooltip delay={0} position="top" title={label} interactive hideOnClick="persistent" className="mr-4">
<div onClick={this.props.togglePlay} className="hover-main color-main cursor-pointer rounded hover:bg-gray-light-shade">
<Icon name={icon} size="36" color="inherit" />
</div>
)}
</div>
</Tooltip>
);
};
<div className="flex items-center h-full">
{ !live && <div className={cn(styles.divider, 'h-full')} /> }
{/* ! TEMP DISABLED !
controlIcon = (icon, size, action, isBackwards, additionalClasses) => (
<div
onClick={action}
className={cn('py-1 px-2 hover-main cursor-pointer', additionalClasses)}
style={{ transform: isBackwards ? 'rotate(180deg)' : '' }}
>
<Icon name={icon} size={size} color="inherit" />
</div>
);
render() {
const {
bottomBlock,
toggleBottomBlock,
live,
livePlay,
skip,
speed,
disabled,
logCount,
logRedCount,
resourceRedCount,
fetchRedCount,
showStack,
stackCount,
stackRedCount,
profilesCount,
storageCount,
showStorage,
storageType,
showProfiler,
showGraphql,
showFetch,
fetchCount,
graphqlCount,
exceptionsCount,
showExceptions,
fullscreen,
inspectorMode,
closedLive,
toggleSpeed,
toggleSkip,
liveTimeTravel,
} = this.props;
const toggleBottomTools = (blockName) => {
if (blockName === INSPECTOR) {
toggleInspectorMode();
bottomBlock && toggleBottomBlock();
} else {
toggleInspectorMode(false);
toggleBottomBlock(blockName);
}
};
return (
<div className={styles.controls}>
{!live || liveTimeTravel ? (
<Timeline jump={this.props.jump} liveTimeTravel={liveTimeTravel} pause={this.props.pause} togglePlay={this.props.togglePlay} />
) : null}
{!fullscreen && (
<div className={cn(styles.buttons, { '!px-5 !pt-0': live })} data-is-live={live}>
<div className="flex items-center">
{!live && (
<>
<PlayerControls
live={live}
skip={skip}
speed={speed}
disabled={disabled}
backTenSeconds={this.backTenSeconds}
forthTenSeconds={this.forthTenSeconds}
toggleSpeed={toggleSpeed}
toggleSkip={toggleSkip}
playButton={this.renderPlayBtn()}
controlIcon={this.controlIcon}
/>
{/* <Button variant="text" onClick={() => toggleBottomTools(OVERVIEW)}>X-RAY</Button> */}
<div className={cn('h-14 border-r bg-gray-light mx-6')} />
<XRayButton isActive={bottomBlock === OVERVIEW && !inspectorMode} onClick={() => toggleBottomTools(OVERVIEW)} />
</>
)}
{live && !closedLive && (
<div className={styles.buttonsLeft}>
<LiveTag isLive={livePlay} onClick={() => (livePlay ? null : jumpToLive())} />
<div className="font-semibold px-2">
<AssistDuration isLivePlay={livePlay} />
</div>
{!liveTimeTravel && (
<div
onClick={toggleTimetravel}
className="p-2 ml-2 rounded hover:bg-teal-light bg-gray-lightest cursor-pointer"
>
See Past Activity
</div>
)}
</div>
)}
</div>
<div className="flex items-center h-full">
{/* { !live && <div className={cn(styles.divider, 'h-full')} /> } */}
{/* ! TEMP DISABLED !
{!live && (
<ControlButton
disabled={ disabled && !inspectorMode }
@ -350,7 +348,7 @@ export default class Controls extends React.Component {
containerClassName="mx-2"
/>
)} */}
<ControlButton
{/* <ControlButton
// disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(OVERVIEW) }
active={ bottomBlock === OVERVIEW && !inspectorMode}
@ -360,132 +358,132 @@ export default class Controls extends React.Component {
// count={ logCount }
// hasErrors={ logRedCount > 0 }
containerClassName="mx-2"
/>
<ControlButton
disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(CONSOLE) }
active={ bottomBlock === CONSOLE && !inspectorMode}
label="CONSOLE"
noIcon
labelClassName="!text-base font-semibold"
count={ logCount }
hasErrors={ logRedCount > 0 }
containerClassName="mx-2"
/>
{ !live &&
<ControlButton
disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(NETWORK) }
active={ bottomBlock === NETWORK && !inspectorMode }
label="NETWORK"
hasErrors={ resourceRedCount > 0 }
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
}
{!live &&
<ControlButton
disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(PERFORMANCE) }
active={ bottomBlock === PERFORMANCE && !inspectorMode }
label="PERFORMANCE"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
}
{showFetch &&
<ControlButton
disabled={disabled && !inspectorMode}
onClick={ ()=> toggleBottomTools(FETCH) }
active={ bottomBlock === FETCH && !inspectorMode }
hasErrors={ fetchRedCount > 0 }
count={ fetchCount }
label="FETCH"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
}
{ !live && showGraphql &&
<ControlButton
disabled={disabled && !inspectorMode}
onClick={ ()=> toggleBottomTools(GRAPHQL) }
active={ bottomBlock === GRAPHQL && !inspectorMode }
count={ graphqlCount }
label="GRAPHQL"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
}
{ !live && showStorage &&
<ControlButton
disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(STORAGE) }
active={ bottomBlock === STORAGE && !inspectorMode }
count={ storageCount }
label={ getStorageName(storageType) }
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
}
{ showExceptions &&
<ControlButton
disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(EXCEPTIONS) }
active={ bottomBlock === EXCEPTIONS && !inspectorMode }
label="EXCEPTIONS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
count={ exceptionsCount }
hasErrors={ exceptionsCount > 0 }
/>
}
{ !live && showStack &&
<ControlButton
disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(STACKEVENTS) }
active={ bottomBlock === STACKEVENTS && !inspectorMode }
label="EVENTS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
count={ stackCount }
hasErrors={ stackRedCount > 0 }
/>
}
{ !live && showProfiler &&
<ControlButton
disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(PROFILER) }
active={ bottomBlock === PROFILER && !inspectorMode }
count={ profilesCount }
label="PROFILER"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
}
{ !live && <div className={cn(styles.divider, 'h-full')} /> }
{ !live && (
<Tooltip
title="Fullscreen"
delay={0}
position="top-end"
className="mx-4"
>
{this.controlIcon("arrows-angle-extend", 18, this.props.fullscreenOn, false, "rounded hover:bg-gray-light-shade color-gray-medium")}
</Tooltip>
)
}
/> */}
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(CONSOLE)}
active={bottomBlock === CONSOLE && !inspectorMode}
label="CONSOLE"
noIcon
labelClassName="!text-base font-semibold"
count={logCount}
hasErrors={logRedCount > 0}
containerClassName="mx-2"
/>
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(NETWORK)}
active={bottomBlock === NETWORK && !inspectorMode}
label="NETWORK"
hasErrors={resourceRedCount > 0}
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(PERFORMANCE)}
active={bottomBlock === PERFORMANCE && !inspectorMode}
label="PERFORMANCE"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{showFetch && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(FETCH)}
active={bottomBlock === FETCH && !inspectorMode}
hasErrors={fetchRedCount > 0}
count={fetchCount}
label="FETCH"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && showGraphql && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(GRAPHQL)}
active={bottomBlock === GRAPHQL && !inspectorMode}
count={graphqlCount}
label="GRAPHQL"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && showStorage && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(STORAGE)}
active={bottomBlock === STORAGE && !inspectorMode}
count={storageCount}
label={getStorageName(storageType)}
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{showExceptions && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(EXCEPTIONS)}
active={bottomBlock === EXCEPTIONS && !inspectorMode}
label="EXCEPTIONS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
count={exceptionsCount}
hasErrors={exceptionsCount > 0}
/>
)}
{!live && showStack && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(STACKEVENTS)}
active={bottomBlock === STACKEVENTS && !inspectorMode}
label="EVENTS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
count={stackCount}
hasErrors={stackRedCount > 0}
/>
)}
{!live && showProfiler && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(PROFILER)}
active={bottomBlock === PROFILER && !inspectorMode}
count={profilesCount}
label="PROFILER"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && <div className={cn(styles.divider, 'h-full')} />}
{!live && (
<Tooltip title="Fullscreen" delay={0} position="top-end" className="mx-4">
{this.controlIcon(
'arrows-angle-extend',
18,
this.props.fullscreenOn,
false,
'rounded hover:bg-gray-light-shade color-gray-medium'
)}
</Tooltip>
)}
</div>
</div>
)}
</div>
</div>
}
</div>
);
}
);
}
}

View file

@ -13,393 +13,392 @@ import { debounce } from 'App/utils';
import { Tooltip } from 'react-tippy';
import TooltipContainer from './components/TooltipContainer';
const BOUNDRY = 15
const BOUNDRY = 0;
function getTimelinePosition(value, scale) {
const pos = value * scale;
const pos = value * scale;
return pos > 100 ? 100 : pos;
return pos > 100 ? 100 : pos;
}
const getPointerIcon = (type) => {
// exception,
switch(type) {
case 'fetch':
return 'funnel/file-earmark-minus-fill';
case 'exception':
return 'funnel/exclamation-circle-fill';
case 'log':
return 'funnel/exclamation-circle-fill';
case 'stack':
return 'funnel/patch-exclamation-fill';
case 'resource':
return 'funnel/file-earmark-minus-fill';
// exception,
switch (type) {
case 'fetch':
return 'funnel/file-earmark-minus-fill';
case 'exception':
return 'funnel/exclamation-circle-fill';
case 'log':
return 'funnel/exclamation-circle-fill';
case 'stack':
return 'funnel/patch-exclamation-fill';
case 'resource':
return 'funnel/file-earmark-minus-fill';
case 'dead_click':
return 'funnel/dizzy';
case 'click_rage':
return 'funnel/dizzy';
case 'excessive_scrolling':
return 'funnel/mouse';
case 'bad_request':
return 'funnel/file-medical-alt';
case 'missing_resource':
return 'funnel/file-earmark-minus-fill';
case 'memory':
return 'funnel/sd-card';
case 'cpu':
return 'funnel/microchip';
case 'slow_resource':
return 'funnel/hourglass-top';
case 'slow_page_load':
return 'funnel/hourglass-top';
case 'crash':
return 'funnel/file-exclamation';
case 'js_exception':
return 'funnel/exclamation-circle-fill';
}
return 'info';
}
case 'dead_click':
return 'funnel/dizzy';
case 'click_rage':
return 'funnel/dizzy';
case 'excessive_scrolling':
return 'funnel/mouse';
case 'bad_request':
return 'funnel/file-medical-alt';
case 'missing_resource':
return 'funnel/file-earmark-minus-fill';
case 'memory':
return 'funnel/sd-card';
case 'cpu':
return 'funnel/microchip';
case 'slow_resource':
return 'funnel/hourglass-top';
case 'slow_page_load':
return 'funnel/hourglass-top';
case 'crash':
return 'funnel/file-exclamation';
case 'js_exception':
return 'funnel/exclamation-circle-fill';
}
return 'info';
};
let deboucneJump = () => null;
let debounceTooltipChange = () => null;
@connectPlayer(state => ({
playing: state.playing,
time: state.time,
skipIntervals: state.skipIntervals,
events: state.eventList,
skip: state.skip,
// not updating properly rn
// skipToIssue: state.skipToIssue,
disabled: state.cssLoading || state.messagesLoading || state.markedTargets,
endTime: state.endTime,
live: state.live,
logList: state.logList,
exceptionsList: state.exceptionsList,
resourceList: state.resourceList,
stackList: state.stackList,
fetchList: state.fetchList,
@connectPlayer((state) => ({
playing: state.playing,
time: state.time,
skipIntervals: state.skipIntervals,
events: state.eventList,
skip: state.skip,
// not updating properly rn
// skipToIssue: state.skipToIssue,
disabled: state.cssLoading || state.messagesLoading || state.markedTargets,
endTime: state.endTime,
live: state.live,
logList: state.logList,
exceptionsList: state.exceptionsList,
resourceList: state.resourceList,
stackList: state.stackList,
fetchList: state.fetchList,
}))
@connect(state => ({
issues: state.getIn([ 'sessions', 'current', 'issues' ]),
clickRageTime: state.getIn([ 'sessions', 'current', 'clickRage' ]) &&
state.getIn([ 'sessions', 'current', 'clickRageTime' ]),
returningLocationTime: state.getIn([ 'sessions', 'current', 'returningLocation' ]) &&
state.getIn([ 'sessions', 'current', 'returningLocationTime' ]),
tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible'])
}), { setTimelinePointer, setTimelineHoverTime })
@connect(
(state) => ({
issues: state.getIn(['sessions', 'current', 'issues']),
clickRageTime: state.getIn(['sessions', 'current', 'clickRage']) && state.getIn(['sessions', 'current', 'clickRageTime']),
returningLocationTime:
state.getIn(['sessions', 'current', 'returningLocation']) && state.getIn(['sessions', 'current', 'returningLocationTime']),
tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible']),
}),
{ setTimelinePointer, setTimelineHoverTime }
)
export default class Timeline extends React.PureComponent {
progressRef = React.createRef()
timelineRef = React.createRef()
wasPlaying = false
progressRef = React.createRef();
timelineRef = React.createRef();
wasPlaying = false;
seekProgress = (e) => {
const time = this.getTime(e)
this.props.jump(time);
this.hideTimeTooltip()
}
seekProgress = (e) => {
const time = this.getTime(e);
this.props.jump(time);
this.hideTimeTooltip();
};
getTime = (e) => {
const { endTime } = this.props;
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
getTime = (e) => {
const { endTime } = this.props;
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
return time
}
return time;
};
createEventClickHandler = pointer => (e) => {
e.stopPropagation();
this.props.jump(pointer.time);
this.props.setTimelinePointer(pointer);
}
createEventClickHandler = (pointer) => (e) => {
e.stopPropagation();
this.props.jump(pointer.time);
this.props.setTimelinePointer(pointer);
};
componentDidMount() {
const { issues } = this.props;
const skipToIssue = Controls.updateSkipToIssue();
const firstIssue = issues.get(0);
deboucneJump = debounce(this.props.jump, 500);
debounceTooltipChange = debounce(this.props.setTimelineHoverTime, 50);
componentDidMount() {
const { issues } = this.props;
const skipToIssue = Controls.updateSkipToIssue();
const firstIssue = issues.get(0);
deboucneJump = debounce(this.props.jump, 500);
debounceTooltipChange = debounce(this.props.setTimelineHoverTime, 50);
if (firstIssue && skipToIssue) {
this.props.jump(firstIssue.time);
if (firstIssue && skipToIssue) {
this.props.jump(firstIssue.time);
}
}
}
onDragEnd = () => {
if (this.wasPlaying) {
this.props.togglePlay();
onDragEnd = () => {
if (this.wasPlaying) {
this.props.togglePlay();
}
};
onDrag = (offset) => {
const { endTime } = this.props;
const p = (offset.x - BOUNDRY) / this.progressRef.current.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
deboucneJump(time);
this.hideTimeTooltip();
if (this.props.playing) {
this.wasPlaying = true;
this.props.pause();
}
};
showTimeTooltip = (e) => {
if (e.target !== this.progressRef.current && e.target !== this.timelineRef.current) {
return this.props.tooltipVisible && this.hideTimeTooltip();
}
const time = this.getTime(e);
const { endTime, liveTimeTravel } = this.props;
const timeLineTooltip = {
time: liveTimeTravel ? endTime - time : time,
offset: e.nativeEvent.offsetX,
isVisible: true,
};
debounceTooltipChange(timeLineTooltip);
};
hideTimeTooltip = () => {
const timeLineTooltip = { isVisible: false };
debounceTooltipChange(timeLineTooltip);
};
render() {
const {
events,
skip,
skipIntervals,
disabled,
endTime,
exceptionsList,
resourceList,
clickRageTime,
stackList,
fetchList,
issues,
liveTimeTravel,
} = this.props;
const scale = 100 / endTime;
return (
<div className="flex items-center absolute w-full" style={{ top: '-4px', zIndex: 100, padding: `0 ${BOUNDRY}px`, maxWidth: '100%' }}>
<div
className={stl.progress}
onClick={disabled ? null : this.seekProgress}
ref={this.progressRef}
role="button"
onMouseMoveCapture={this.showTimeTooltip}
onMouseEnter={this.showTimeTooltip}
onMouseLeave={this.hideTimeTooltip}
>
<TooltipContainer liveTimeTravel={liveTimeTravel} />
{/* custo color is live */}
<DraggableCircle left={this.props.time * scale} onDrop={this.onDragEnd} />
<CustomDragLayer
onDrag={this.onDrag}
minX={BOUNDRY}
maxX={this.progressRef.current && this.progressRef.current.offsetWidth + BOUNDRY}
/>
<TimeTracker scale={scale} />
{skip &&
skipIntervals.map((interval) => (
<div
key={interval.start}
className={stl.skipInterval}
style={{
left: `${getTimelinePosition(interval.start, scale)}%`,
width: `${(interval.end - interval.start) * scale}%`,
}}
/>
))}
<div className={stl.timeline} ref={this.timelineRef} />
{events.map((e) => (
<div key={e.key} className={stl.event} style={{ left: `${getTimelinePosition(e.time, scale)}%` }} />
))}
{/* {issues.map((iss) => (
<div
style={{
left: `${getTimelinePosition(iss.time, scale)}%`,
top: '0px',
zIndex: 11,
width: 16,
height: 16,
}}
key={iss.key}
className={stl.clickRage}
onClick={this.createEventClickHandler(iss)}
>
<Tooltip
delay={0}
position="top"
html={
<div className={stl.popup}>
<b>{iss.name}</b>
</div>
}
>
<Icon className="rounded-full bg-white" name={iss.icon} size="16" />
</Tooltip>
</div>
))}
{events
.filter((e) => e.type === TYPES.CLICKRAGE)
.map((e) => (
<div
style={{
left: `${getTimelinePosition(e.time, scale)}%`,
top: '0px',
zIndex: 11,
width: 16,
height: 16,
}}
key={e.key}
className={stl.clickRage}
onClick={this.createEventClickHandler(e)}
>
<Tooltip
delay={0}
position="top"
html={
<div className={stl.popup}>
<b>{'Click Rage'}</b>
</div>
}
>
<Icon className="bg-white" name={getPointerIcon('click_rage')} color="red" size="16" />
</Tooltip>
</div>
))}
{typeof clickRageTime === 'number' && (
<div
style={{
left: `${getTimelinePosition(clickRageTime, scale)}%`,
top: '-0px',
zIndex: 11,
width: 16,
height: 16,
}}
className={stl.clickRage}
>
<Tooltip
delay={0}
position="top"
html={
<div className={stl.popup}>
<b>{'Click Rage'}</b>
</div>
}
>
<Icon className="rounded-full bg-white" name={getPointerIcon('click_rage')} color="red" size="16" />
</Tooltip>
</div>
)}
{exceptionsList.map((e) => (
<div
key={e.key}
className={cn(stl.markup, stl.error)}
style={{ left: `${getTimelinePosition(e.time, scale)}%`, top: '0px', zIndex: 10, width: 16, height: 16 }}
onClick={this.createEventClickHandler(e)}
>
<Tooltip
delay={0}
position="top"
html={
<div className={stl.popup}>
<b>{'Exception'}</b>
<br />
<span>{e.message}</span>
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('exception')} color="red" size="16" />
</Tooltip>
</div>
))}
{resourceList
.filter((r) => r.isRed() || r.isYellow())
.map((r) => (
<div
key={r.key}
className={cn(stl.markup, {
[stl.error]: r.isRed(),
[stl.warning]: r.isYellow(),
})}
style={{ left: `${getTimelinePosition(r.time, scale)}%`, top: '0px', zIndex: 10, width: 16, height: 16 }}
onClick={this.createEventClickHandler(r)}
>
<Tooltip
delay={0}
position="top"
html={
<div className={stl.popup}>
<b>{r.success ? 'Slow resource: ' : 'Missing resource:'}</b>
<br />
{r.name}
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('resource')} size="16" />
</Tooltip>
</div>
))}
{fetchList
.filter((e) => e.isRed())
.map((e) => (
<div
key={e.key}
className={cn(stl.markup, stl.error)}
style={{ left: `${getTimelinePosition(e.time, scale)}%`, top: '0px' }}
onClick={this.createEventClickHandler(e)}
>
<Tooltip
delay={0}
position="top"
html={
<div className={stl.popup}>
<b>Failed Fetch</b>
<br />
{e.name}
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('fetch')} color="red" size="16" />
</Tooltip>
</div>
))}
{stackList
.filter((e) => e.isRed())
.map((e) => (
<div
key={e.key}
className={cn(stl.markup, stl.error)}
style={{ left: `${getTimelinePosition(e.time, scale)}%`, top: '0px' }}
onClick={this.createEventClickHandler(e)}
>
<Tooltip
delay={0}
position="top"
html={
<div className={stl.popup}>
<b>Stack Event</b>
<br />
{e.name}
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('stack')} size="16" />
</Tooltip>
</div>
))} */}
</div>
</div>
);
}
}
onDrag = (offset) => {
const { endTime } = this.props;
const p = (offset.x - BOUNDRY) / this.progressRef.current.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
deboucneJump(time);
this.hideTimeTooltip();
if (this.props.playing) {
this.wasPlaying = true;
this.props.pause();
}
}
showTimeTooltip = (e) => {
if (e.target !== this.progressRef.current && e.target !== this.timelineRef.current) {
return this.props.tooltipVisible && this.hideTimeTooltip()
}
const time = this.getTime(e);
const { endTime, liveTimeTravel } = this.props;
const timeLineTooltip = {
time: liveTimeTravel ? endTime - time : time,
offset: e.nativeEvent.offsetX,
isVisible: true
}
debounceTooltipChange(timeLineTooltip)
}
hideTimeTooltip = () => {
const timeLineTooltip = { isVisible: false }
debounceTooltipChange(timeLineTooltip)
}
render() {
const {
events,
skip,
skipIntervals,
disabled,
endTime,
exceptionsList,
resourceList,
clickRageTime,
stackList,
fetchList,
issues,
liveTimeTravel,
} = this.props;
const scale = 100 / endTime;
return (
<div
className="flex items-center absolute w-full"
style={{ top: '-4px', zIndex: 100, padding: `0 ${BOUNDRY}px`, maxWidth: '100%' }}
>
<div
className={ stl.progress }
onClick={ disabled ? null : this.seekProgress }
ref={ this.progressRef }
role="button"
onMouseMoveCapture={this.showTimeTooltip}
onMouseEnter={ this.showTimeTooltip}
onMouseLeave={this.hideTimeTooltip}
>
<TooltipContainer liveTimeTravel={liveTimeTravel} />
{/* custo color is live */}
<DraggableCircle left={this.props.time * scale} onDrop={this.onDragEnd} />
<CustomDragLayer onDrag={this.onDrag} minX={BOUNDRY} maxX={this.progressRef.current && this.progressRef.current.offsetWidth + BOUNDRY} />
<TimeTracker scale={ scale } />
{ skip && skipIntervals.map(interval =>
(<div
key={ interval.start }
className={ stl.skipInterval }
style={ {
left: `${getTimelinePosition(interval.start, scale)}%`,
width: `${ (interval.end - interval.start) * scale }%`,
} }
/>))
}
<div className={ stl.timeline } ref={this.timelineRef} />
{ events.map(e => (
<div
key={ e.key }
className={ stl.event }
style={ { left: `${ getTimelinePosition(e.time,scale)}%` } }
/>
))
}
{
issues.map(iss => (
<div
style={ {
left: `${ getTimelinePosition(iss.time, scale) }%`,
top: '0px',
zIndex: 11, width: 16, height: 16
} }
key={iss.key}
className={ stl.clickRage }
onClick={ this.createEventClickHandler(iss) }
>
<Tooltip
delay={0}
position="top"
html={
<div className={ stl.popup }>
<b>{ iss.name }</b>
</div>
}
>
<Icon className="rounded-full bg-white" name={iss.icon} size="16" />
</Tooltip>
</div>
))
}
{ events.filter(e => e.type === TYPES.CLICKRAGE).map(e => (
<div
style={ {
left: `${ getTimelinePosition(e.time, scale) }%`,
top: '0px',
zIndex: 11, width: 16, height: 16
} }
key={e.key}
className={ stl.clickRage }
onClick={ this.createEventClickHandler(e) }
>
<Tooltip
delay={0}
position="top"
html={
<div className={ stl.popup }>
<b>{ "Click Rage" }</b>
</div>
}
>
<Icon className="bg-white" name={getPointerIcon('click_rage')} color="red" size="16" />
</Tooltip>
</div>
))}
{typeof clickRageTime === 'number' &&
<div
style={{
left: `${ getTimelinePosition(clickRageTime, scale) }%`,
top: '-0px',
zIndex: 11, width: 16, height: 16
}}
className={stl.clickRage}
>
<Tooltip
delay={0}
position="top"
html={
<div className={ stl.popup }>
<b>{ "Click Rage" }</b>
</div>
}
>
<Icon className="rounded-full bg-white" name={getPointerIcon('click_rage')} color="red" size="16" />
</Tooltip>
</div>
}
{ exceptionsList
.map(e => (
<div
key={ e.key }
className={ cn(stl.markup, stl.error) }
style={ { left: `${ getTimelinePosition(e.time, scale) }%`, top: '0px', zIndex: 10, width: 16, height: 16 } }
onClick={ this.createEventClickHandler(e) }
>
<Tooltip
delay={0}
position="top"
html={
<div className={ stl.popup } >
<b>{ "Exception" }</b>
<br/>
<span>{ e.message }</span>
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('exception')} color="red" size="16" />
</Tooltip>
</div>
))
}
{ resourceList
.filter(r => r.isRed() || r.isYellow())
.map(r => (
<div
key={ r.key }
className={ cn(stl.markup, {
[ stl.error ]: r.isRed(),
[ stl.warning ]: r.isYellow(),
}) }
style={ { left: `${ getTimelinePosition(r.time, scale) }%`, top: '0px', zIndex: 10, width: 16, height: 16 } }
onClick={ this.createEventClickHandler(r) }
>
<Tooltip
delay={0}
position="top"
html={
<div className={ stl.popup }>
<b>{ r.success ? "Slow resource: " : "Missing resource:" }</b>
<br/>
{ r.name }
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('resource')} size="16" />
</Tooltip>
</div>
))
}
{ fetchList
.filter(e => e.isRed())
.map(e => (
<div
key={ e.key }
className={ cn(stl.markup, stl.error) }
style={ { left: `${ getTimelinePosition(e.time, scale) }%`, top: '0px' } }
onClick={ this.createEventClickHandler(e) }
>
<Tooltip
delay={0}
position="top"
html={
<div className={ stl.popup }>
<b>Failed Fetch</b>
<br/>
{ e.name }
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('fetch')} color="red" size="16" />
</Tooltip>
</div>
))
}
{ stackList
.filter(e => e.isRed())
.map(e => (
<div
key={ e.key }
className={ cn(stl.markup, stl.error) }
style={ { left: `${ getTimelinePosition(e.time, scale) }%`, top: '0px' } }
onClick={ this.createEventClickHandler(e) }
>
<Tooltip
delay={0}
position="top"
html={
<div className={ stl.popup }>
<b>Stack Event</b>
<br/>
{ e.name }
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('stack')} size="16" />
</Tooltip>
</div>
))
}
</div>
</div>
);
}
}

View file

@ -1,111 +1,72 @@
import React from 'react'
import React from 'react';
import { Tooltip } from 'react-tippy';
import { ReduxTime } from '../Time';
import { Icon } from 'UI';
import cn from 'classnames';
// @ts-ignore
import styles from '../controls.module.css'
import styles from '../controls.module.css';
interface Props {
live: boolean;
skip: boolean;
speed: number;
disabled: boolean;
playButton: JSX.Element;
backTenSeconds: () => void;
forthTenSeconds: () => void;
toggleSpeed: () => void;
toggleSkip: () => void;
controlIcon: (icon: string, size: number, action: () => void, isBackwards: boolean, additionalClasses: string) => JSX.Element;
live: boolean;
skip: boolean;
speed: number;
disabled: boolean;
playButton: JSX.Element;
backTenSeconds: () => void;
forthTenSeconds: () => void;
toggleSpeed: () => void;
toggleSkip: () => void;
controlIcon: (icon: string, size: number, action: () => void, isBackwards: boolean, additionalClasses: string) => JSX.Element;
}
function PlayerControls(props: Props) {
const {
live,
skip,
speed,
disabled,
playButton,
backTenSeconds,
forthTenSeconds,
toggleSpeed,
toggleSkip,
controlIcon
} = props;
return (
<div className="flex items-center">
{playButton}
{!live && (
<div className="flex items-center font-semibold text-center" style={{ minWidth: 85 }}>
{/* @ts-ignore */}
<ReduxTime isCustom name="time" format="mm:ss" />
<span className="px-1">/</span>
{/* @ts-ignore */}
<ReduxTime isCustom name="endTime" format="mm:ss" />
const { live, skip, speed, disabled, playButton, backTenSeconds, forthTenSeconds, toggleSpeed, toggleSkip, controlIcon } = props;
return (
<div className="flex items-center">
{playButton}
{!live && (
<div className="flex items-center font-semibold text-center" style={{ minWidth: 85 }}>
{/* @ts-ignore */}
<ReduxTime isCustom name="time" format="mm:ss" />
<span className="px-1">/</span>
{/* @ts-ignore */}
<ReduxTime isCustom name="endTime" format="mm:ss" />
</div>
)}
<div className="rounded ml-4 bg-active-blue border border-active-blue-border flex items-stretch">
{/* @ts-ignore */}
<Tooltip title="Rewind 10s" delay={0} position="top">
{controlIcon('skip-forward-fill', 18, backTenSeconds, true, 'hover:bg-active-blue-border color-main h-full flex items-center')}
</Tooltip>
<div className="p-1 border-l border-r bg-active-blue-border border-active-blue-border">10s</div>
{/* @ts-ignore */}
<Tooltip title="Forward 10s" delay={0} position="top">
{controlIcon('skip-forward-fill', 18, forthTenSeconds, false, 'hover:bg-active-blue-border color-main h-full flex items-center')}
</Tooltip>
</div>
{!live && (
<div className="flex items-center ml-4">
{/* @ts-ignore */}
<Tooltip title="Playback speed" delay={0} position="top">
<button className={styles.speedButton} onClick={toggleSpeed} data-disabled={disabled}>
<div>{speed + 'x'}</div>
</button>
</Tooltip>
<button
className={cn(styles.skipIntervalButton, { [styles.withCheckIcon]: skip, [styles.active]: skip }, 'ml-4')}
onClick={toggleSkip}
data-disabled={disabled}
>
{skip && <Icon name="check" size="24" className="mr-1" />}
{'Skip Inactivity'}
</button>
</div>
)}
</div>
)}
<div className="rounded ml-4 bg-active-blue border border-active-blue-border flex items-stretch">
{/* @ts-ignore */}
<Tooltip
title='Rewind 10s'
delay={0}
position="top"
>
{controlIcon(
"skip-forward-fill",
18,
backTenSeconds,
true,
'hover:bg-active-blue-border color-main h-full flex items-center'
)}
</Tooltip>
<div className='p-1 border-l border-r bg-active-blue-border border-active-blue-border'>10s</div>
{/* @ts-ignore */}
<Tooltip
title='Forward 10s'
delay={0}
position="top"
>
{controlIcon(
"skip-forward-fill",
18,
forthTenSeconds,
false,
'hover:bg-active-blue-border color-main h-full flex items-center'
)}
</Tooltip>
</div>
{!live &&
<div className='flex items-center mx-4'>
{/* @ts-ignore */}
<Tooltip
title='Playback speed'
delay={0}
position="top"
>
<button
className={styles.speedButton}
onClick={toggleSpeed}
data-disabled={disabled}
>
<div>{speed + 'x'}</div>
</button>
</Tooltip>
<button
className={cn(styles.skipIntervalButton, { [styles.withCheckIcon]: skip, [styles.active]: skip }, 'ml-4')}
onClick={toggleSkip}
data-disabled={disabled}
>
{skip && <Icon name="check" size="24" className="mr-1" />}
{'Skip Inactivity'}
</button>
</div>
}
</div>
)
);
}
export default PlayerControls;

View file

@ -104,9 +104,9 @@ export default class Player extends React.PureComponent {
ref={ this.screenWrapper }
/>
</div>
{ // !fullscreen && !!bottomBlock &&
{ !fullscreen && !!bottomBlock &&
<div style={{ maxWidth, width: '100%' }}>
{ // bottomBlock === OVERVIEW &&
{ bottomBlock === OVERVIEW &&
<OverviewPanel />
}
{ bottomBlock === CONSOLE &&

View file

@ -0,0 +1,18 @@
import React from 'react';
import stl from './xrayButton.module.css';
import cn from 'classnames';
interface Props {
onClick?: () => void;
isActive?: boolean;
}
function XRayButton(props: Props) {
const { isActive } = props;
return (
<button className={cn(stl.wrapper, { [stl.default] : !isActive, [stl.active] : isActive})} onClick={props.onClick}>
X-RAY
</button>
);
}
export default XRayButton;

View file

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

View file

@ -0,0 +1,17 @@
.wrapper {
text-align: center;
padding: 4px 14px;
border: none;
border-radius: 6px;
font-weight: 500;
&.default {
color: white;
background: linear-gradient(90deg, rgba(57, 78, 255, 0.87) 0%, rgba(62, 170, 175, 0.87) 100%);
}
&.active {
background: rgba(63, 81, 181, 0.08);
color: $gray-darkest;
}
}

View file

@ -356,3 +356,9 @@ export function getTimelinePosition(value: any, scale: any) {
const pos = value * scale;
return pos > 100 ? 100 : pos;
}
export function millisToMinutesAndSeconds(millis: any) {
const minutes = Math.floor(millis / 60000);
const seconds: any = ((millis % 60000) / 1000).toFixed(0);
return minutes + 'm' + (seconds < 10 ? '0' : '') + seconds + 's';
}