Merge pull request #680 from openreplay/timeline-overview
feat(ui) - xray (timeline overview)
This commit is contained in:
commit
a7743b426e
50 changed files with 1927 additions and 1103 deletions
|
|
@ -2,82 +2,77 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import withSiteIdRouter from 'HOCs/withSiteIdRouter';
|
||||
import { errors as errorsRoute, error as errorRoute } from 'App/routes';
|
||||
import { NoContent , Loader, IconButton, Icon, Popup, BackLink, } from 'UI';
|
||||
import { NoContent, Loader, IconButton, Icon, Popup, BackLink } from 'UI';
|
||||
import { fetch, fetchTrace } from 'Duck/errors';
|
||||
import MainSection from './MainSection';
|
||||
import SideSection from './SideSection';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
@connect(state =>({
|
||||
errorIdInStore: state.getIn(["errors", "instance"]).errorId,
|
||||
loading: state.getIn([ "errors", "fetch", "loading" ]) || state.getIn([ "errors", "fetchTrace", "loading" ]),
|
||||
errorOnFetch: state.getIn(["errors", "fetch", "errors"]) || state.getIn([ "errors", "fetchTrace", "errors" ]),
|
||||
}), {
|
||||
fetch,
|
||||
fetchTrace,
|
||||
})
|
||||
@connect(
|
||||
(state) => ({
|
||||
errorIdInStore: state.getIn(['errors', 'instance']).errorId,
|
||||
loading: state.getIn(['errors', 'fetch', 'loading']) || state.getIn(['errors', 'fetchTrace', 'loading']),
|
||||
errorOnFetch: state.getIn(['errors', 'fetch', 'errors']) || state.getIn(['errors', 'fetchTrace', 'errors']),
|
||||
}),
|
||||
{
|
||||
fetch,
|
||||
fetchTrace,
|
||||
}
|
||||
)
|
||||
@withSiteIdRouter
|
||||
export default class ErrorInfo extends React.PureComponent {
|
||||
ensureInstance() {
|
||||
const { errorId, loading, errorOnFetch } = this.props;
|
||||
if (!loading &&
|
||||
this.props.errorIdInStore !== errorId &&
|
||||
errorId != null) {
|
||||
this.props.fetch(errorId);
|
||||
this.props.fetchTrace(errorId)
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
this.ensureInstance();
|
||||
}
|
||||
componentDidUpdate() {
|
||||
this.ensureInstance();
|
||||
}
|
||||
next = () => {
|
||||
const { list, errorId } = this.props;
|
||||
const curIndex = list.findIndex(e => e.errorId === errorId);
|
||||
const next = list.get(curIndex + 1);
|
||||
if (next != null) {
|
||||
this.props.history.push(errorRoute(next.errorId))
|
||||
}
|
||||
}
|
||||
prev = () => {
|
||||
const { list, errorId } = this.props;
|
||||
const curIndex = list.findIndex(e => e.errorId === errorId);
|
||||
const prev = list.get(curIndex - 1);
|
||||
if (prev != null) {
|
||||
this.props.history.push(errorRoute(prev.errorId))
|
||||
}
|
||||
|
||||
}
|
||||
render() {
|
||||
const {
|
||||
loading,
|
||||
errorIdInStore,
|
||||
list,
|
||||
errorId,
|
||||
} = this.props;
|
||||
ensureInstance() {
|
||||
const { errorId, loading, errorOnFetch } = this.props;
|
||||
if (!loading && this.props.errorIdInStore !== errorId && errorId != null) {
|
||||
this.props.fetch(errorId);
|
||||
this.props.fetchTrace(errorId);
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
this.ensureInstance();
|
||||
}
|
||||
componentDidUpdate() {
|
||||
this.ensureInstance();
|
||||
}
|
||||
next = () => {
|
||||
const { list, errorId } = this.props;
|
||||
const curIndex = list.findIndex((e) => e.errorId === errorId);
|
||||
const next = list.get(curIndex + 1);
|
||||
if (next != null) {
|
||||
this.props.history.push(errorRoute(next.errorId));
|
||||
}
|
||||
};
|
||||
prev = () => {
|
||||
const { list, errorId } = this.props;
|
||||
const curIndex = list.findIndex((e) => e.errorId === errorId);
|
||||
const prev = list.get(curIndex - 1);
|
||||
if (prev != null) {
|
||||
this.props.history.push(errorRoute(prev.errorId));
|
||||
}
|
||||
};
|
||||
render() {
|
||||
const { loading, errorIdInStore, list, errorId } = this.props;
|
||||
|
||||
let nextDisabled = true,
|
||||
prevDisabled = true;
|
||||
if (list.size > 0) {
|
||||
nextDisabled = loading || list.last().errorId === errorId;
|
||||
prevDisabled = loading || list.first().errorId === errorId;
|
||||
}
|
||||
let nextDisabled = true,
|
||||
prevDisabled = true;
|
||||
if (list.size > 0) {
|
||||
nextDisabled = loading || list.last().errorId === errorId;
|
||||
prevDisabled = loading || list.first().errorId === errorId;
|
||||
}
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
|
||||
<div className="mt-6 text-2xl">No Error Found!</div>
|
||||
</div>
|
||||
}
|
||||
subtext="Please try to find existing one."
|
||||
// animatedIcon="no-results"
|
||||
show={ !loading && errorIdInStore == null }
|
||||
>
|
||||
{/* <div className="w-9/12 mb-4 flex justify-between">
|
||||
return (
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
|
||||
<div className="mt-6 text-2xl">No Error Found!</div>
|
||||
</div>
|
||||
}
|
||||
subtext="Please try to find existing one."
|
||||
// animatedIcon="no-results"
|
||||
show={!loading && errorIdInStore == null}
|
||||
>
|
||||
{/* <div className="w-9/12 mb-4 flex justify-between">
|
||||
<BackLink to={ errorsRoute() } label="Back" />
|
||||
<div />
|
||||
<div className="flex items-center">
|
||||
|
|
@ -111,13 +106,13 @@ export default class ErrorInfo extends React.PureComponent {
|
|||
</Popup>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="flex" >
|
||||
<Loader loading={ loading } className="w-9/12">
|
||||
<MainSection className="w-9/12" />
|
||||
<SideSection className="w-3/12" />
|
||||
</Loader>
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
<div className="flex">
|
||||
<Loader loading={loading} className="w-9/12">
|
||||
<MainSection className="w-9/12" />
|
||||
<SideSection className="w-3/12" />
|
||||
</Loader>
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,105 +5,89 @@ import withSiteIdRouter from 'HOCs/withSiteIdRouter';
|
|||
import { ErrorDetails, IconButton, Icon, Loader, Button } from 'UI';
|
||||
import { sessions as sessionsRoute } from 'App/routes';
|
||||
import { TYPES as EV_FILER_TYPES } from 'Types/filter/event';
|
||||
import { UNRESOLVED, RESOLVED, IGNORED } from "Types/errorInfo";
|
||||
import { UNRESOLVED, RESOLVED, IGNORED } from 'Types/errorInfo';
|
||||
import { addFilterByKeyAndValue } from 'Duck/search';
|
||||
import { resolve,unresolve,ignore, toggleFavorite } from "Duck/errors";
|
||||
import { resolve, unresolve, ignore, toggleFavorite } from 'Duck/errors';
|
||||
import { resentOrDate } from 'App/date';
|
||||
import Divider from 'Components/Errors/ui/Divider';
|
||||
import ErrorName from 'Components/Errors/ui/ErrorName';
|
||||
import Label from 'Components/Errors/ui/Label';
|
||||
import SharePopup from 'Shared/SharePopup'
|
||||
import SharePopup from 'Shared/SharePopup';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
|
||||
import SessionBar from './SessionBar';
|
||||
|
||||
@withSiteIdRouter
|
||||
@connect(state => ({
|
||||
error: state.getIn([ "errors", "instance" ]),
|
||||
trace: state.getIn([ "errors", "instanceTrace" ]),
|
||||
sourcemapUploaded: state.getIn([ "errors", "sourcemapUploaded" ]),
|
||||
resolveToggleLoading: state.getIn(["errors", "resolve", "loading"]) ||
|
||||
state.getIn(["errors", "unresolve", "loading"]),
|
||||
ignoreLoading: state.getIn([ "errors", "ignore", "loading" ]),
|
||||
toggleFavoriteLoading: state.getIn([ "errors", "toggleFavorite", "loading" ]),
|
||||
traceLoading: state.getIn([ "errors", "fetchTrace", "loading"]),
|
||||
}),{
|
||||
resolve,
|
||||
unresolve,
|
||||
ignore,
|
||||
toggleFavorite,
|
||||
addFilterByKeyAndValue,
|
||||
})
|
||||
@connect(
|
||||
(state) => ({
|
||||
error: state.getIn(['errors', 'instance']),
|
||||
trace: state.getIn(['errors', 'instanceTrace']),
|
||||
sourcemapUploaded: state.getIn(['errors', 'sourcemapUploaded']),
|
||||
resolveToggleLoading: state.getIn(['errors', 'resolve', 'loading']) || state.getIn(['errors', 'unresolve', 'loading']),
|
||||
ignoreLoading: state.getIn(['errors', 'ignore', 'loading']),
|
||||
toggleFavoriteLoading: state.getIn(['errors', 'toggleFavorite', 'loading']),
|
||||
traceLoading: state.getIn(['errors', 'fetchTrace', 'loading']),
|
||||
}),
|
||||
{
|
||||
resolve,
|
||||
unresolve,
|
||||
ignore,
|
||||
toggleFavorite,
|
||||
addFilterByKeyAndValue,
|
||||
}
|
||||
)
|
||||
export default class MainSection extends React.PureComponent {
|
||||
resolve = () => {
|
||||
const { error } = this.props;
|
||||
this.props.resolve(error.errorId)
|
||||
}
|
||||
resolve = () => {
|
||||
const { error } = this.props;
|
||||
this.props.resolve(error.errorId);
|
||||
};
|
||||
|
||||
unresolve = () => {
|
||||
const { error } = this.props;
|
||||
this.props.unresolve(error.errorId)
|
||||
}
|
||||
unresolve = () => {
|
||||
const { error } = this.props;
|
||||
this.props.unresolve(error.errorId);
|
||||
};
|
||||
|
||||
ignore = () => {
|
||||
const { error } = this.props;
|
||||
this.props.ignore(error.errorId)
|
||||
}
|
||||
bookmark = () => {
|
||||
const { error } = this.props;
|
||||
this.props.toggleFavorite(error.errorId);
|
||||
}
|
||||
ignore = () => {
|
||||
const { error } = this.props;
|
||||
this.props.ignore(error.errorId);
|
||||
};
|
||||
bookmark = () => {
|
||||
const { error } = this.props;
|
||||
this.props.toggleFavorite(error.errorId);
|
||||
};
|
||||
|
||||
findSessions = () => {
|
||||
this.props.addFilterByKeyAndValue(FilterKey.ERROR, this.props.error.message);
|
||||
this.props.history.push(sessionsRoute());
|
||||
}
|
||||
findSessions = () => {
|
||||
this.props.addFilterByKeyAndValue(FilterKey.ERROR, this.props.error.message);
|
||||
this.props.history.push(sessionsRoute());
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
error,
|
||||
trace,
|
||||
sourcemapUploaded,
|
||||
ignoreLoading,
|
||||
resolveToggleLoading,
|
||||
toggleFavoriteLoading,
|
||||
className,
|
||||
traceLoading,
|
||||
} = this.props;
|
||||
render() {
|
||||
const { error, trace, sourcemapUploaded, ignoreLoading, resolveToggleLoading, toggleFavoriteLoading, className, traceLoading } = this.props;
|
||||
|
||||
return (
|
||||
<div className={cn(className, "bg-white border-radius-3 thin-gray-border mb-6")} >
|
||||
<div className="m-4">
|
||||
<ErrorName
|
||||
className="text-lg leading-relaxed"
|
||||
name={ error.name }
|
||||
message={ error.stack0InfoString }
|
||||
lineThrough={ error.status === RESOLVED }
|
||||
/>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center color-gray-dark" style={{ wordBreak: 'break-all'}}>
|
||||
{ error.message }
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="flex">
|
||||
<Label
|
||||
topValue={ error.sessions }
|
||||
topValueSize="text-lg"
|
||||
bottomValue="Sessions"
|
||||
/>
|
||||
<Label
|
||||
topValue={ error.users }
|
||||
topValueSize="text-lg"
|
||||
bottomValue="Users"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs color-gray-medium">Over the past 30 days</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
return (
|
||||
<div className={cn(className, 'bg-white border-radius-3 thin-gray-border mb-6')}>
|
||||
<div className="m-4">
|
||||
<ErrorName
|
||||
className="text-lg leading-relaxed"
|
||||
name={error.name}
|
||||
message={error.stack0InfoString}
|
||||
lineThrough={error.status === RESOLVED}
|
||||
/>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center color-gray-dark" style={{ wordBreak: 'break-all' }}>
|
||||
{error.message}
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="flex">
|
||||
<Label topValue={error.sessions} topValueSize="text-lg" bottomValue="Sessions" />
|
||||
<Label topValue={error.users} topValueSize="text-lg" bottomValue="Users" />
|
||||
</div>
|
||||
<div className="text-xs color-gray-medium">Over the past 30 days</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <Divider />
|
||||
{/* <Divider />
|
||||
<div className="flex m-4">
|
||||
{ error.status === UNRESOLVED
|
||||
? <IconButton
|
||||
|
|
@ -158,35 +142,29 @@ export default class MainSection extends React.PureComponent {
|
|||
}
|
||||
/>
|
||||
</div> */}
|
||||
<Divider />
|
||||
<div className="m-4">
|
||||
<h3 className="text-xl inline-block mr-2">Last session with this error</h3>
|
||||
<span className="font-thin text-sm">{ resentOrDate(error.lastOccurrence) }</span>
|
||||
<SessionBar
|
||||
className="my-4"
|
||||
session={ error.lastHydratedSession }
|
||||
/>
|
||||
<Button
|
||||
variant="text-primary"
|
||||
onClick={ this.findSessions }
|
||||
>
|
||||
Find all sessions with this error
|
||||
<Icon className="ml-1" name="next1" color="teal" />
|
||||
</Button>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="m-4">
|
||||
<Loader loading={ traceLoading }>
|
||||
<ErrorDetails
|
||||
name={error.name}
|
||||
message={error.message}
|
||||
errorStack={trace}
|
||||
sourcemapUploaded={sourcemapUploaded}
|
||||
/>
|
||||
</Loader>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
<Divider />
|
||||
<div className="m-4">
|
||||
<h3 className="text-xl inline-block mr-2">Last session with this error</h3>
|
||||
<span className="font-thin text-sm">{resentOrDate(error.lastOccurrence)}</span>
|
||||
<SessionBar className="my-4" session={error.lastHydratedSession} />
|
||||
<Button variant="text-primary" onClick={this.findSessions}>
|
||||
Find all sessions with this error
|
||||
<Icon className="ml-1" name="next1" color="teal" />
|
||||
</Button>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="m-4">
|
||||
<Loader loading={traceLoading}>
|
||||
<ErrorDetails
|
||||
name={error.name}
|
||||
message={error.message}
|
||||
errorStack={trace}
|
||||
error={error}
|
||||
sourcemapUploaded={sourcemapUploaded}
|
||||
/>
|
||||
</Loader>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ const Header = (props) => {
|
|||
</div>
|
||||
|
||||
<ul>
|
||||
<li><button onClick={ onLogoutClick }>{ 'Account' }</button></li>
|
||||
<li><button onClick={ onLogoutClick }>{ 'Logout' }</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ function LivePlayer ({
|
|||
}, [])
|
||||
|
||||
const TABS = {
|
||||
EVENTS: 'Events',
|
||||
EVENTS: 'User Actions',
|
||||
HEATMAPS: 'Click Map',
|
||||
}
|
||||
const [activeTab, setActiveTab] = useState('');
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import cn from 'classnames';
|
|||
import stl from './bottomBlock.module.css';
|
||||
|
||||
const BottomBlock = ({
|
||||
children,
|
||||
className,
|
||||
additionalHeight,
|
||||
children = null,
|
||||
className = '',
|
||||
additionalHeight = 0,
|
||||
...props
|
||||
}) => (
|
||||
<div className={ cn(stl.wrapper, "flex flex-col mb-2") } { ...props } >
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export default class Exceptions extends React.PureComponent {
|
|||
show={ !loading && errorStack.size === 0 }
|
||||
title="Nothing found!"
|
||||
>
|
||||
<ErrorDetails error={ currentError.name } errorStack={errorStack} sourcemapUploaded={sourcemapUploaded} />
|
||||
<ErrorDetails error={ currentError } errorStack={errorStack} sourcemapUploaded={sourcemapUploaded} />
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
|
|
@ -73,24 +73,27 @@ export default class Exceptions extends React.PureComponent {
|
|||
/>
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
<Input
|
||||
// className="input-small"
|
||||
placeholder="Filter by name or message"
|
||||
icon="search"
|
||||
iconPosition="left"
|
||||
name="filter"
|
||||
onChange={ this.onFilterChange }
|
||||
/>
|
||||
<div className="mr-8">
|
||||
<QuestionMarkHint
|
||||
onHover={true}
|
||||
content={
|
||||
<>
|
||||
<a className="color-teal underline" target="_blank" href="https://docs.openreplay.com/installation/upload-sourcemaps">Upload Source Maps </a>
|
||||
and see source code context obtained from stack traces in their original form.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div></div>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
// className="input-small"
|
||||
placeholder="Filter by name or message"
|
||||
icon="search"
|
||||
iconPosition="left"
|
||||
name="filter"
|
||||
onChange={ this.onFilterChange }
|
||||
/>
|
||||
<div className="mx-4">
|
||||
<QuestionMarkHint
|
||||
onHover={true}
|
||||
content={
|
||||
<>
|
||||
<a className="color-teal underline" target="_blank" href="https://docs.openreplay.com/installation/upload-sourcemaps">Upload Source Maps </a>
|
||||
and see source code context obtained from stack traces in their original form.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export default class Fetch extends React.PureComponent {
|
|||
/>
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
<h4 className="text-lg">Fetch</h4>
|
||||
<span className="font-semibold color-gray-medium mr-4">Fetch</span>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
className="input-small"
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export default class GraphQL extends React.PureComponent {
|
|||
/>
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
<h4 className="text-lg">GraphQL</h4>
|
||||
<span className="font-semibold color-gray-medium mr-4">GraphQL</span>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
// className="input-small"
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export default class GraphQL extends React.PureComponent {
|
|||
return (
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
<h4 className="text-lg">Long Tasks</h4>
|
||||
<span className="font-semibold color-gray-medium mr-4">Long Tasks</span>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
className="input-small mr-3"
|
||||
|
|
|
|||
108
frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx
Normal file
108
frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { connectPlayer } from 'App/player';
|
||||
import { toggleBottomBlock } from 'Duck/components/player';
|
||||
import React, { useEffect } from 'react';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import EventRow from './components/EventRow';
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import { connect } from 'react-redux';
|
||||
import TimelineScale from './components/TimelineScale';
|
||||
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 OverviewPanelContainer from './components/OverviewPanelContainer';
|
||||
|
||||
interface Props {
|
||||
resourceList: any[];
|
||||
exceptionsList: any[];
|
||||
eventsList: any[];
|
||||
toggleBottomBlock: any;
|
||||
stackEventList: any[];
|
||||
issuesList: any[];
|
||||
performanceChartData: any;
|
||||
endTime: number;
|
||||
}
|
||||
function OverviewPanel(props: Props) {
|
||||
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: eventsList.filter((item: any) => item.type === TYPES.CLICKRAGE),
|
||||
PERFORMANCE: performanceChartData,
|
||||
};
|
||||
}, [dataLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.resourceList.length > 0) {
|
||||
setDataLoaded(true);
|
||||
}
|
||||
}, [props.resourceList]);
|
||||
|
||||
return (
|
||||
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>
|
||||
</OverviewPanelContainer>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
</Wrapper>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: any) => ({
|
||||
issuesList: state.getIn(['sessions', 'current', 'issues']),
|
||||
}),
|
||||
{
|
||||
toggleBottomBlock,
|
||||
}
|
||||
)(
|
||||
connectPlayer((state: any) => ({
|
||||
resourceList: state.resourceList.filter((r: any) => r.isRed() || r.isYellow()),
|
||||
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>;
|
||||
});
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
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, isGraph = false } = props;
|
||||
const scale = 100 / endTime;
|
||||
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: '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 EventRow;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './EventRow';
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import React from 'react';
|
||||
import { Checkbox } from 'UI';
|
||||
|
||||
const NETWORK = 'NETWORK';
|
||||
const ERRORS = 'ERRORS';
|
||||
const EVENTS = 'EVENTS';
|
||||
const CLICKRAGE = 'CLICKRAGE';
|
||||
const PERFORMANCE = 'PERFORMANCE';
|
||||
|
||||
interface Props {
|
||||
list: any[];
|
||||
updateList: any;
|
||||
}
|
||||
function FeatureSelection(props: Props) {
|
||||
const { list } = props;
|
||||
const features = [NETWORK, ERRORS, EVENTS, CLICKRAGE, PERFORMANCE];
|
||||
const disabled = list.length >= 3;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{features.map((feature, index) => {
|
||||
const checked = list.includes(feature);
|
||||
const _disabled = disabled && !checked;
|
||||
return (
|
||||
<Checkbox
|
||||
key={index}
|
||||
label={feature}
|
||||
checked={checked}
|
||||
className="mx-4"
|
||||
disabled={_disabled}
|
||||
onClick={() => {
|
||||
if (checked) {
|
||||
props.updateList(list.filter((item: any) => item !== feature));
|
||||
} else {
|
||||
props.updateList([...list, feature]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default FeatureSelection;
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import VerticalLine from '../VerticalLine';
|
||||
import { connectPlayer, Controls } from 'App/player';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
endTime: number;
|
||||
}
|
||||
|
||||
const OverviewPanelContainer = React.memo((props: Props) => {
|
||||
const { endTime } = props;
|
||||
const [mouseX, setMouseX] = React.useState(0);
|
||||
const [mouseIn, setMouseIn] = React.useState(false);
|
||||
const onClickTrack = (e: any) => {
|
||||
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
|
||||
const time = Math.max(Math.round(p * endTime), 0);
|
||||
if (time) {
|
||||
Controls.jump(time);
|
||||
}
|
||||
};
|
||||
|
||||
// const 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}
|
||||
// onMouseOver={() => setMouseIn(true)}
|
||||
// onMouseOut={() => setMouseIn(false)}
|
||||
>
|
||||
{mouseIn && <VerticalLine left={mouseX} className="border-gray-medium" />}
|
||||
<div className="">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default OverviewPanelContainer;
|
||||
|
||||
// export default connectPlayer((state: any) => ({
|
||||
// endTime: state.endTime,
|
||||
// }))(OverviewPanelContainer);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './OverviewPanelContainer';
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './PerformanceGraph';
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import JsonViewer from './components/JsonViewer';
|
||||
import Sentry from './components/Sentry';
|
||||
import { OPENREPLAY, SENTRY, DATADOG, STACKDRIVER } from 'Types/session/stackEvent';
|
||||
|
||||
interface Props {
|
||||
event: any;
|
||||
}
|
||||
function StackEventModal(props: Props) {
|
||||
const renderPopupContent = () => {
|
||||
const {
|
||||
event: { source, payload, name },
|
||||
} = props;
|
||||
switch (source) {
|
||||
case SENTRY:
|
||||
return <Sentry event={payload} />;
|
||||
case DATADOG:
|
||||
return <JsonViewer title={name} data={payload} icon="integrations/datadog" />;
|
||||
case STACKDRIVER:
|
||||
return <JsonViewer title={name} data={payload} icon="integrations/stackdriver" />;
|
||||
default:
|
||||
return <JsonViewer title={name} data={payload} icon={`integrations/${source}`} />;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '450px' }}>
|
||||
{renderPopupContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StackEventModal;
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import { Icon, JSONTree } from 'UI';
|
||||
|
||||
export default class JsonViewer extends React.PureComponent {
|
||||
render() {
|
||||
const { data, title, icon } = this.props;
|
||||
return (
|
||||
<div className="p-5">
|
||||
<Icon name={icon} size="30" />
|
||||
<h4 className="my-5 capitalize"> {title}</h4>
|
||||
<JSONTree src={data} collapsed={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './JsonViewer';
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import React from 'react';
|
||||
import { getIn, get } from 'immutable';
|
||||
import cn from 'classnames';
|
||||
import { withRequest } from 'HOCs';
|
||||
import { Loader, Icon, JSONTree } from 'UI';
|
||||
import { Accordion } from 'semantic-ui-react';
|
||||
import stl from './sentry.module.css';
|
||||
|
||||
@withRequest({
|
||||
endpoint: (props) => `/integrations/sentry/events/${props.event.id}`,
|
||||
dataName: 'detailedEvent',
|
||||
loadOnInitialize: true,
|
||||
})
|
||||
export default class SentryEventInfo extends React.PureComponent {
|
||||
makePanelsFromStackTrace(stacktrace) {
|
||||
return get(stacktrace, 'frames', []).map(({ filename, function: method, lineNo, context = [] }) => ({
|
||||
key: `${filename}_${method}_${lineNo}`,
|
||||
title: {
|
||||
content: (
|
||||
<span className={stl.accordionTitle}>
|
||||
<b>{filename}</b>
|
||||
{' in '}
|
||||
<b>{method}</b>
|
||||
{' at line '}
|
||||
<b>{lineNo}</b>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
content: {
|
||||
content: (
|
||||
<ol start={getIn(context, [0, 0], 0)} className={stl.lineList}>
|
||||
{context.map(([ctxLineNo, codeText]) => (
|
||||
<li className={cn(stl.codeLine, { [stl.highlighted]: ctxLineNo === lineNo })}>{codeText}</li>
|
||||
))}
|
||||
</ol>
|
||||
),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
renderBody() {
|
||||
const { detailedEvent, requestError, event } = this.props;
|
||||
|
||||
const exceptionEntry = get(detailedEvent, ['entries'], []).find(({ type }) => type === 'exception');
|
||||
const stacktraces = getIn(exceptionEntry, ['data', 'values']);
|
||||
if (!stacktraces) {
|
||||
return <JSONTree src={requestError ? event : detailedEvent} sortKeys={false} enableClipboard />;
|
||||
}
|
||||
return stacktraces.map(({ type, value, stacktrace }) => (
|
||||
<div key={type} className={stl.stacktrace}>
|
||||
<h6>{type}</h6>
|
||||
<p>{value}</p>
|
||||
<Accordion styled panels={this.makePanelsFromStackTrace(stacktrace)} />
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { open, toggleOpen, loading } = this.props;
|
||||
return (
|
||||
<div className={stl.wrapper}>
|
||||
<Icon name="integrations/sentry-text" size="30" color="gray-medium" />
|
||||
<Loader loading={loading}>{this.renderBody()}</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Sentry';
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
|
||||
.wrapper {
|
||||
padding: 20px 40px 30px;
|
||||
}
|
||||
.icon {
|
||||
margin-left: -5px;
|
||||
}
|
||||
.stacktrace {
|
||||
& h6 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 17px;
|
||||
padding-top: 7px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
& p {
|
||||
font-family: 'Menlo', 'monaco', 'consolas', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.accordionTitle {
|
||||
font-weight: 100;
|
||||
& > b {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.lineList {
|
||||
list-style-position: inside;
|
||||
list-style-type: decimal-leading-zero;
|
||||
background: $gray-lightest;
|
||||
}
|
||||
|
||||
.codeLine {
|
||||
font-family: 'Menlo', 'monaco', 'consolas', monospace;
|
||||
line-height: 24px;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
min-height: 24px;
|
||||
padding: 0 25px;
|
||||
&.highlighted {
|
||||
background: $red;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './StackEventModal';
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import React from 'react';
|
||||
import { connectPlayer, Controls } from 'App/player';
|
||||
import { toggleBottomBlock, NETWORK, EXCEPTIONS, PERFORMANCE } from 'Duck/components/player';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { Icon, ErrorDetails, Popup } from 'UI';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import { TYPES as EVENT_TYPES } from 'Types/session/event';
|
||||
import StackEventModal from '../StackEventModal';
|
||||
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
|
||||
|
||||
interface Props {
|
||||
pointer: any;
|
||||
type: any;
|
||||
}
|
||||
const TimelinePointer = React.memo((props: Props) => {
|
||||
const { showModal, hideModal } = useModal();
|
||||
const createEventClickHandler = (pointer: any, type: any) => (e: any) => {
|
||||
e.stopPropagation();
|
||||
Controls.jump(pointer.time);
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'ERRORS') {
|
||||
showModal(<ErrorDetailsModal errorId={pointer.errorId} />, { right: true });
|
||||
}
|
||||
|
||||
if (type === 'EVENT') {
|
||||
showModal(<StackEventModal event={pointer} />, { right: true });
|
||||
}
|
||||
// props.toggleBottomBlock(type);
|
||||
};
|
||||
|
||||
const renderNetworkElement = (item: any) => {
|
||||
return (
|
||||
<Popup
|
||||
content={
|
||||
<div className="">
|
||||
<b>{item.success ? 'Slow resource: ' : 'Missing resource:'}</b>
|
||||
<br />
|
||||
{item.name}
|
||||
</div>
|
||||
}
|
||||
delay={0}
|
||||
position="top"
|
||||
>
|
||||
<div onClick={createEventClickHandler(item, NETWORK)} className="cursor-pointer">
|
||||
<div className="h-3 w-3 rounded-full bg-red" />
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
|
||||
const renderClickRageElement = (item: any) => {
|
||||
return (
|
||||
<Popup
|
||||
content={
|
||||
<div className="">
|
||||
<b>{'Click Rage'}</b>
|
||||
</div>
|
||||
}
|
||||
delay={0}
|
||||
position="top"
|
||||
>
|
||||
<div onClick={createEventClickHandler(item, null)} className="cursor-pointer">
|
||||
<Icon className="bg-white" name="funnel/emoji-angry" color="red" size="16" />
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStackEventElement = (item: any) => {
|
||||
return (
|
||||
<Popup
|
||||
content={
|
||||
<div className="">
|
||||
<b>{'Stack Event'}</b>
|
||||
</div>
|
||||
}
|
||||
delay={0}
|
||||
position="top"
|
||||
>
|
||||
<div onClick={createEventClickHandler(item, 'EVENT')} className="cursor-pointer w-1 h-4 bg-red">
|
||||
{/* <Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" /> */}
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPerformanceElement = (item: any) => {
|
||||
return (
|
||||
<Popup
|
||||
content={
|
||||
<div className="">
|
||||
<b>{item.type}</b>
|
||||
</div>
|
||||
}
|
||||
delay={0}
|
||||
position="top"
|
||||
>
|
||||
<div onClick={createEventClickHandler(item, EXCEPTIONS)} className="cursor-pointer w-1 h-4 bg-red">
|
||||
{/* <Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" /> */}
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
|
||||
const renderExceptionElement = (item: any) => {
|
||||
return (
|
||||
<Popup
|
||||
content={
|
||||
<div className="">
|
||||
<b>{'Exception'}</b>
|
||||
<br />
|
||||
<span>{item.message}</span>
|
||||
</div>
|
||||
}
|
||||
delay={0}
|
||||
position="top"
|
||||
>
|
||||
<div onClick={createEventClickHandler(item, 'ERRORS')} className="cursor-pointer">
|
||||
<Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" />
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
const { pointer, type } = props;
|
||||
if (type === 'NETWORK') {
|
||||
return renderNetworkElement(pointer);
|
||||
}
|
||||
if (type === 'CLICKRAGE') {
|
||||
return renderClickRageElement(pointer);
|
||||
}
|
||||
if (type === 'ERRORS') {
|
||||
return renderExceptionElement(pointer);
|
||||
}
|
||||
if (type === 'EVENTS') {
|
||||
return renderStackEventElement(pointer);
|
||||
}
|
||||
|
||||
if (type === 'PERFORMANCE') {
|
||||
return renderPerformanceElement(pointer);
|
||||
}
|
||||
};
|
||||
return <div>{render()}</div>;
|
||||
});
|
||||
|
||||
export default TimelinePointer;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './TimelinePointer'
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import { connectPlayer } from 'App/player';
|
||||
import { millisToMinutesAndSeconds } from 'App/utils';
|
||||
|
||||
interface Props {
|
||||
endTime: number;
|
||||
}
|
||||
function TimelineScale(props: Props) {
|
||||
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 connectPlayer((state: any) => ({
|
||||
// endTime: state.endTime,
|
||||
// }))(TimelineScale);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './TimelineScale';
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
|
||||
interface Props {
|
||||
left: number;
|
||||
className?: string;
|
||||
height?: string;
|
||||
width?: string;
|
||||
}
|
||||
function VerticalLine(props: Props) {
|
||||
const { left, className = 'border-gray-dark', height = '221px', width = '1px' } = props;
|
||||
return <div className={cn('absolute border-r border-dashed z-10', className)} style={{ left: `${left}%`, height, width }} />;
|
||||
}
|
||||
|
||||
export default VerticalLine;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './VerticalLine'
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import { connectPlayer } from 'App/player';
|
||||
import VerticalLine from '../VerticalLine';
|
||||
|
||||
interface Props {
|
||||
time: number;
|
||||
scale: number;
|
||||
}
|
||||
function VerticalPointerLine(props: Props) {
|
||||
const { time, scale } = props;
|
||||
const left = time * scale;
|
||||
return <VerticalLine left={left} className="border-teal" />;
|
||||
}
|
||||
|
||||
export default connectPlayer((state: any) => ({
|
||||
time: state.time,
|
||||
scale: 100 / state.endTime,
|
||||
}))(VerticalPointerLine);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './VerticalPointerLine'
|
||||
1
frontend/app/components/Session_/OverviewPanel/index.ts
Normal file
1
frontend/app/components/Session_/OverviewPanel/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './OverviewPanel';
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
.popup {
|
||||
max-width: 300px !important;
|
||||
/* max-height: 300px !important; */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
& span {
|
||||
display: block;
|
||||
max-height: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +1,51 @@
|
|||
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 {
|
||||
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,
|
||||
changeSkipInterval,
|
||||
CONSOLE,
|
||||
NETWORK,
|
||||
STACKEVENTS,
|
||||
STORAGE,
|
||||
PROFILER,
|
||||
PERFORMANCE,
|
||||
GRAPHQL,
|
||||
FETCH,
|
||||
EXCEPTIONS,
|
||||
INSPECTOR,
|
||||
fullscreenOn,
|
||||
fullscreenOff,
|
||||
toggleBottomBlock,
|
||||
changeSkipInterval,
|
||||
OVERVIEW,
|
||||
CONSOLE,
|
||||
NETWORK,
|
||||
STACKEVENTS,
|
||||
STORAGE,
|
||||
PROFILER,
|
||||
PERFORMANCE,
|
||||
GRAPHQL,
|
||||
FETCH,
|
||||
EXCEPTIONS,
|
||||
INSPECTOR,
|
||||
} from 'Duck/components/player';
|
||||
import { 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';
|
||||
}
|
||||
}
|
||||
|
||||
const SKIP_INTERVALS = {
|
||||
|
|
@ -66,296 +59,301 @@ const SKIP_INTERVALS = {
|
|||
};
|
||||
|
||||
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' ]),
|
||||
skipInterval: state.getIn(['components', 'player', 'skipInterval']),
|
||||
}
|
||||
}, {
|
||||
fullscreenOn,
|
||||
fullscreenOff,
|
||||
toggleBottomBlock,
|
||||
changeSkipInterval,
|
||||
})
|
||||
@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']),
|
||||
skipInterval: state.getIn(['components', 'player', 'skipInterval']),
|
||||
};
|
||||
},
|
||||
{
|
||||
fullscreenOn,
|
||||
fullscreenOff,
|
||||
toggleBottomBlock,
|
||||
changeSkipInterval,}
|
||||
)
|
||||
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 ||
|
||||
nextProps.skipInterval !== this.props.skipInterval
|
||||
) 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||
|
||||
nextProps.skipInterval !== this.props.skipInterval)
|
||||
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, skipInterval } = this.props;
|
||||
jump(Math.min(endTime, time + SKIP_INTERVALS[skipInterval]))
|
||||
}
|
||||
forthTenSeconds = () => {
|
||||
const { time, endTime, jump, skipInterval } = this.props;
|
||||
jump(Math.min(endTime, time + SKIP_INTERVALS[skipInterval]));
|
||||
};
|
||||
|
||||
backTenSeconds = () => { //shouldComponentUpdate
|
||||
const { time, jump, skipInterval } = this.props;
|
||||
jump(Math.max(0, time - SKIP_INTERVALS[skipInterval]));
|
||||
}
|
||||
backTenSeconds = () => {
|
||||
//shouldComponentUpdate
|
||||
const { time, jump, skipInterval } = this.props;
|
||||
jump(Math.max(0, time - SKIP_INTERVALS[skipInterval]));
|
||||
};
|
||||
|
||||
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"
|
||||
>
|
||||
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={this.props.togglePlay}
|
||||
className="hover-main color-main cursor-pointer rounded hover:bg-gray-light-shade"
|
||||
onClick={action}
|
||||
className={cn('py-1 px-2 hover-main cursor-pointer', additionalClasses)}
|
||||
style={{ transform: isBackwards ? 'rotate(180deg)' : '' }}
|
||||
>
|
||||
<Icon name={icon} size="36" color="inherit" />
|
||||
<Icon name={icon} size={size} 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,
|
||||
changeSkipInterval,
|
||||
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,
|
||||
changeSkipInterval,
|
||||
skipInterval,
|
||||
} = this.props;
|
||||
|
||||
const toggleBottomTools = (blockName) => {
|
||||
if (blockName === INSPECTOR) {
|
||||
toggleInspectorMode();
|
||||
bottomBlock && toggleBottomBlock();
|
||||
} else {
|
||||
toggleInspectorMode(false);
|
||||
toggleBottomBlock(blockName);
|
||||
}
|
||||
}
|
||||
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}
|
||||
ref={this.speedRef}
|
||||
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}
|
||||
ref={this.speedRef}
|
||||
skipIntervals={SKIP_INTERVALS}
|
||||
setSkipInterval={changeSkipInterval}
|
||||
currentInterval={skipInterval}
|
||||
/>
|
||||
)}
|
||||
currentInterval={skipInterval}/>
|
||||
{/* <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>
|
||||
{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>
|
||||
{!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 !
|
||||
<div className="flex items-center h-full">
|
||||
{/* { !live && <div className={cn(styles.divider, 'h-full')} /> } */}
|
||||
{/* ! TEMP DISABLED !
|
||||
{!live && (
|
||||
<ControlButton
|
||||
disabled={ disabled && !inspectorMode }
|
||||
|
|
@ -367,131 +365,142 @@ export default class Controls extends React.Component {
|
|||
containerClassName="mx-2"
|
||||
/>
|
||||
)} */}
|
||||
<ControlButton
|
||||
disabled={ disabled && !inspectorMode }
|
||||
onClick={ () => toggleBottomTools(CONSOLE) }
|
||||
active={ bottomBlock === CONSOLE && !inspectorMode}
|
||||
label="CONSOLE"
|
||||
{/* <ControlButton
|
||||
// disabled={ disabled && !inspectorMode }
|
||||
onClick={ () => toggleBottomTools(OVERVIEW) }
|
||||
active={ bottomBlock === OVERVIEW && !inspectorMode}
|
||||
label="OVERVIEW"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
count={ logCount }
|
||||
hasErrors={ logRedCount > 0 }
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,10 @@ function PlayerControls(props: Props) {
|
|||
<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">
|
||||
<button ref={arrowBackRef} className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent">
|
||||
<button
|
||||
ref={arrowBackRef}
|
||||
className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent"
|
||||
>
|
||||
{controlIcon(
|
||||
'skip-forward-fill',
|
||||
18,
|
||||
|
|
@ -123,7 +126,10 @@ function PlayerControls(props: Props) {
|
|||
</div>
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip title="Forward 10s" delay={0} position="top">
|
||||
<button ref={arrowForwardRef} className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent">
|
||||
<button
|
||||
ref={arrowForwardRef}
|
||||
className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent"
|
||||
>
|
||||
{controlIcon(
|
||||
'skip-forward-fill',
|
||||
18,
|
||||
|
|
@ -136,7 +142,7 @@ function PlayerControls(props: Props) {
|
|||
</div>
|
||||
|
||||
{!live && (
|
||||
<div className="flex items-center mx-4">
|
||||
<div className="flex items-center ml-4">
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip title="Control play back speed (↑↓)" delay={0} position="top">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
EXCEPTIONS,
|
||||
LONGTASKS,
|
||||
INSPECTOR,
|
||||
OVERVIEW,
|
||||
} from 'Duck/components/player';
|
||||
import Network from '../Network';
|
||||
import Console from '../Console/Console';
|
||||
|
|
@ -40,6 +41,7 @@ import Controls from './Controls';
|
|||
import Overlay from './Overlay';
|
||||
import stl from './player.module.css';
|
||||
import { updateLastPlayedSession } from 'Duck/sessions';
|
||||
import OverviewPanel from '../OverviewPanel';
|
||||
|
||||
@connectPlayer(state => ({
|
||||
live: state.live,
|
||||
|
|
@ -104,6 +106,9 @@ export default class Player extends React.PureComponent {
|
|||
</div>
|
||||
{ !fullscreen && !!bottomBlock &&
|
||||
<div style={{ maxWidth, width: '100%' }}>
|
||||
{ bottomBlock === OVERVIEW &&
|
||||
<OverviewPanel />
|
||||
}
|
||||
{ bottomBlock === CONSOLE &&
|
||||
<Console />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import cn from "classnames";
|
|||
import { connect } from 'react-redux';
|
||||
import { } from 'Player';
|
||||
import {
|
||||
NONE,
|
||||
NONE, OVERVIEW,
|
||||
} from 'Duck/components/player';
|
||||
import Player from './Player';
|
||||
import SubHeader from './Subheader';
|
||||
|
|
@ -38,6 +38,7 @@ export default class PlayerBlock extends React.PureComponent {
|
|||
<Player
|
||||
className="flex-1"
|
||||
bottomBlockIsActive={ !fullscreen && bottomBlock !== NONE }
|
||||
// bottomBlockIsActive={ true }
|
||||
bottomBlock={bottomBlock}
|
||||
fullscreen={fullscreen}
|
||||
activeTab={activeTab}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,9 @@ export default class Profiler extends React.PureComponent {
|
|||
/>
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
<h4 className="text-lg">Profiler</h4>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">Profiler</span>
|
||||
</div>
|
||||
<Input
|
||||
// className="input-small"
|
||||
placeholder="Filter by Name"
|
||||
|
|
|
|||
18
frontend/app/components/shared/XRayButton/XRayButton.tsx
Normal file
18
frontend/app/components/shared/XRayButton/XRayButton.tsx
Normal 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;
|
||||
1
frontend/app/components/shared/XRayButton/index.ts
Normal file
1
frontend/app/components/shared/XRayButton/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './XRayButton';
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,19 +2,16 @@ import React from 'react';
|
|||
import cn from 'classnames';
|
||||
|
||||
interface Props {
|
||||
classNam?: string;
|
||||
label?: string;
|
||||
[x: string]: any;
|
||||
classNam?: string;
|
||||
label?: string;
|
||||
[x: string]: any;
|
||||
}
|
||||
export default (props: Props) => {
|
||||
const { className = '', label = '', ...rest } = props;
|
||||
return (
|
||||
<label className={ cn("flex items-center cursor-pointer", className)}>
|
||||
<input
|
||||
type="checkbox"
|
||||
{ ...rest }
|
||||
/>
|
||||
{label && <span className="ml-2 select-none mb-0">{label}</span>}
|
||||
</label>
|
||||
)
|
||||
};
|
||||
const { className = '', label = '', ...rest } = props;
|
||||
return (
|
||||
<label className={cn('flex items-center cursor-pointer', className)}>
|
||||
<input type="checkbox" {...rest} />
|
||||
{label && <span className="ml-2 select-none mb-0">{label}</span>}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import ErrorFrame from '../ErrorFrame/ErrorFrame'
|
||||
import cn from 'classnames';
|
||||
import { IconButton, Icon } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const docLink = 'https://docs.openreplay.com/installation/upload-sourcemaps';
|
||||
|
||||
function ErrorDetails({ className, name = "Error", message, errorStack, sourcemapUploaded }) {
|
||||
const [showRaw, setShowRaw] = useState(false)
|
||||
const firstFunc = errorStack.first() && errorStack.first().function
|
||||
|
||||
const openDocs = () => {
|
||||
window.open(docLink, '_blank');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} >
|
||||
{ !sourcemapUploaded && (
|
||||
<div
|
||||
style={{ backgroundColor: 'rgba(204, 0, 0, 0.1)' }}
|
||||
className="font-normal flex items-center text-sm font-regular color-red border p-2 rounded"
|
||||
>
|
||||
<Icon name="info" size="16" color="red" />
|
||||
<div className="ml-2">Source maps must be uploaded to OpenReplay to be able to see stack traces. <a href="#" className="color-red font-medium underline" style={{ textDecoration: 'underline' }} onClick={openDocs}>Learn more.</a></div>
|
||||
</div>
|
||||
) }
|
||||
<div className="flex items-center my-3">
|
||||
<h3 className="text-xl mr-auto">
|
||||
Stacktrace
|
||||
</h3>
|
||||
<div className="flex justify-end mr-2">
|
||||
<IconButton
|
||||
onClick={() => setShowRaw(false) }
|
||||
label="FULL"
|
||||
plain={!showRaw}
|
||||
primaryText={!showRaw}
|
||||
/>
|
||||
<IconButton
|
||||
primaryText={showRaw}
|
||||
onClick={() => setShowRaw(true) }
|
||||
plain={showRaw}
|
||||
label="RAW"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 code-font" data-hidden={showRaw}>
|
||||
<div className="leading-relaxed font-weight-bold">{ name }</div>
|
||||
<div style={{ wordBreak: 'break-all'}}>{message}</div>
|
||||
</div>
|
||||
{ showRaw &&
|
||||
<div className="mb-3 code-font">{name} : {firstFunc ? firstFunc : '?' }</div>
|
||||
}
|
||||
{ errorStack.map((frame, i) => (
|
||||
<div className="mb-3" key={frame.key}>
|
||||
<ErrorFrame frame={frame} showRaw={showRaw} isFirst={i == 0} />
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ErrorDetails.displayName = "ErrorDetails";
|
||||
export default ErrorDetails;
|
||||
82
frontend/app/components/ui/ErrorDetails/ErrorDetails.tsx
Normal file
82
frontend/app/components/ui/ErrorDetails/ErrorDetails.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import ErrorFrame from '../ErrorFrame/ErrorFrame';
|
||||
import { fetchErrorStackList } from 'Duck/sessions';
|
||||
import { Button, Icon } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const docLink = 'https://docs.openreplay.com/installation/upload-sourcemaps';
|
||||
|
||||
interface Props {
|
||||
fetchErrorStackList: any;
|
||||
sourcemapUploaded?: boolean;
|
||||
errorStack?: any;
|
||||
message?: string;
|
||||
sessionId: string;
|
||||
error: any;
|
||||
}
|
||||
function ErrorDetails(props: Props) {
|
||||
const { error, sessionId, message = '', errorStack = [], sourcemapUploaded = false } = props;
|
||||
const [showRaw, setShowRaw] = useState(false);
|
||||
const firstFunc = errorStack.first() && errorStack.first().function;
|
||||
|
||||
const openDocs = () => {
|
||||
window.open(docLink, '_blank');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchErrorStackList(sessionId, error.errorId);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-white p-5 h-screen">
|
||||
{!sourcemapUploaded && (
|
||||
<div
|
||||
style={{ backgroundColor: 'rgba(204, 0, 0, 0.1)' }}
|
||||
className="font-normal flex items-center text-sm font-regular color-red border p-2 rounded"
|
||||
>
|
||||
<Icon name="info" size="16" color="red" />
|
||||
<div className="ml-2">
|
||||
Source maps must be uploaded to OpenReplay to be able to see stack traces.{' '}
|
||||
<a href="#" className="color-red font-medium underline" style={{ textDecoration: 'underline' }} onClick={openDocs}>
|
||||
Learn more.
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center my-3">
|
||||
<h3 className="text-xl mr-auto">Stacktrace</h3>
|
||||
<div className="flex justify-end mr-2">
|
||||
<Button variant={!showRaw ? 'text-primary' : 'text'} onClick={() => setShowRaw(false)}>
|
||||
FULL
|
||||
</Button>
|
||||
<Button variant={showRaw ? 'text-primary' : 'text'} onClick={() => setShowRaw(true)}>
|
||||
RAW
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 code-font" data-hidden={showRaw}>
|
||||
<div className="leading-relaxed font-weight-bold">{error.name}</div>
|
||||
<div style={{ wordBreak: 'break-all' }}>{message}</div>
|
||||
</div>
|
||||
{showRaw && (
|
||||
<div className="mb-3 code-font">
|
||||
{error.name} : {firstFunc ? firstFunc : '?'}
|
||||
</div>
|
||||
)}
|
||||
{errorStack.map((frame: any, i: any) => (
|
||||
<div className="mb-3" key={frame.key}>
|
||||
<ErrorFrame frame={frame} showRaw={showRaw} isFirst={i == 0} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ErrorDetails.displayName = 'ErrorDetails';
|
||||
export default connect(
|
||||
(state: any) => ({
|
||||
errorStack: state.getIn(['sessions', 'errorStack']),
|
||||
sessionId: state.getIn(['sessions', 'current', 'sessionId']),
|
||||
}),
|
||||
{ fetchErrorStackList }
|
||||
)(ErrorDetails);
|
||||
|
|
@ -12,6 +12,7 @@ export const FETCH = 8;
|
|||
export const EXCEPTIONS = 9;
|
||||
export const LONGTASKS = 10;
|
||||
export const INSPECTOR = 11;
|
||||
export const OVERVIEW = 12;
|
||||
|
||||
const TOGGLE_FULLSCREEN = 'player/TOGGLE_FS';
|
||||
const TOGGLE_BOTTOM_BLOCK = 'player/SET_BOTTOM_BLOCK';
|
||||
|
|
|
|||
|
|
@ -64,9 +64,9 @@ export function getRE(string: string, options: string) {
|
|||
}
|
||||
|
||||
export const filterList = <T extends Record<string, any>>(
|
||||
list: T[],
|
||||
searchQuery: string,
|
||||
testKeys: string[],
|
||||
list: T[],
|
||||
searchQuery: string,
|
||||
testKeys: string[],
|
||||
searchCb?: (listItem: T, query: string | RegExp
|
||||
) => boolean): T[] => {
|
||||
const filterRE = getRE(searchQuery, 'i');
|
||||
|
|
@ -337,8 +337,12 @@ export const fetchErrorCheck = async (response: any) => {
|
|||
export const cleanSessionFilters = (data: any) => {
|
||||
const { filters, ...rest } = data;
|
||||
const _fitlers = filters.filter((f: any) => {
|
||||
if (f.operator === 'isAny' || f.operator === 'onAny') { return true } // ignore filter with isAny/onAny operator
|
||||
if (Array.isArray(f.filters) && f.filters.length > 0) { return true } // ignore subfilters
|
||||
if (f.operator === 'isAny' || f.operator === 'onAny') {
|
||||
return true;
|
||||
} // ignore filter with isAny/onAny operator
|
||||
if (Array.isArray(f.filters) && f.filters.length > 0) {
|
||||
return true;
|
||||
} // ignore subfilters
|
||||
|
||||
return f.value !== '' && Array.isArray(f.value) && f.value.length > 0;
|
||||
});
|
||||
|
|
@ -361,3 +365,13 @@ export const getInitials = (name: any) => {
|
|||
const names = name.split(' ');
|
||||
return names.slice(0, 2).map((n: any) => n[0]).join('');
|
||||
}
|
||||
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';
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue