change(ui) - dev tools

This commit is contained in:
Shekar Siri 2022-10-11 10:36:20 +02:00
parent 9b6be7e580
commit a6227c7265
28 changed files with 1417 additions and 302 deletions

View file

@ -112,15 +112,15 @@ export default class Autoscroll extends React.PureComponent<Props, {
{children}
</div>
<div className={stl.navButtons}>
{/* <label><input type={'checkbox'} checked={this.state.autoScroll} onChange={(e) => this.setState({ autoScroll: !this.state.autoScroll })} /> Autoscroll</label> */}
{/* <div className={stl.navButtons}>
<label><input type={'checkbox'} checked={this.state.autoScroll} onChange={(e) => this.setState({ autoScroll: !this.state.autoScroll })} /> Autoscroll</label>
{navigation && (
<>
<IconButton size="small" icon="chevron-up" onClick={this.onPrevClick} />
<IconButton size="small" icon="chevron-down" onClick={this.onNextClick} className="mt-5" />
</>
)}
</div>
</div> */}
</div>
);

View file

@ -6,7 +6,7 @@
display: flex;
align-items: center;
& >.infoPoint {
font-size: 12px;
font-size: 14px;
display: flex;
align-items: center;
&:not(:last-child):after {

View file

@ -83,11 +83,12 @@ export default class ConsoleContent extends React.PureComponent {
<Tabs tabs={TABS} active={activeTab} onClick={this.onTabClick} border={false} />
</div>
<Input
className="input-small"
className="input-small h-8"
placeholder="Filter by keyword"
icon="search"
iconPosition="left"
name="filter"
height={28}
onChange={this.onFilterChange}
/>
</BottomBlock.Header>
@ -101,7 +102,7 @@ export default class ConsoleContent extends React.PureComponent {
<Autoscroll autoScrollTo={Math.max(lastIndex, 0)}>
{filtered.map((l, index) => (
<div
className={cn('flex py-2 px-4', {
className={cn(stl.line, 'flex py-2 px-4', {
info: !l.isYellow() && !l.isRed(),
warn: l.isYellow(),
error: l.isRed(),
@ -114,10 +115,10 @@ export default class ConsoleContent extends React.PureComponent {
<div className={cn(stl.timestamp)}>
<Icon size="14" className={stl.icon} {...getIconProps(l.level)} />
</div>
<div className={cn(stl.timestamp, {})}>
{/* <div className={cn(stl.timestamp, {})}>
{Duration.fromMillis(l.time).toFormat('mm:ss.SSS')}
</div>
<div key={l.key} className={cn(stl.line)} data-scroll-item={l.isRed()}>
</div> */}
<div key={l.key} className={cn('')} data-scroll-item={l.isRed()}>
<div className={stl.message}>{renderWithNL(l.value)}</div>
</div>
</div>

View file

@ -15,6 +15,9 @@
display: flex;
align-items: flex-start;
border-bottom: solid thin $gray-light-shade;
&:hover {
background-coor: $active-blue !important;
}
}
.timestamp {

View file

@ -1,7 +1,7 @@
import React from 'react';
import cn from 'classnames';
// import { connectPlayer } from 'Player';
import { QuestionMarkHint, Popup, Tabs, Input, NoContent, Icon, Button } from 'UI';
import { QuestionMarkHint, Popup, Tabs, Input, NoContent, Icon, Toggler, Button } from 'UI';
import { getRE } from 'App/utils';
import { TYPES } from 'Types/session/resource';
import { formatBytes } from 'App/utils';
@ -48,22 +48,17 @@ export function renderType(r) {
export function renderName(r) {
return (
<Popup
style={{ width: '100%' }}
content={<div className={stl.popupNameContent}>{r.url}</div>}
>
<div className={stl.popupNameTrigger}>{r.name}</div>
</Popup>
<Popup style={{ width: '100%' }} content={<div className={stl.popupNameContent}>{r.url}</div>}>
<div className={stl.popupNameTrigger}>{r.name}</div>
</Popup>
);
}
export function renderStart(r) {
return (
<div className="flex justify-between items-center grow-0 w-full">
<span>
{Duration.fromMillis(r.time).toFormat('mm:ss.SSS')}
</span>
<Button
<span>{Duration.fromMillis(r.time).toFormat('mm:ss.SSS')}</span>
{/* <Button
variant="text"
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
onClick={(e) => {
@ -72,9 +67,9 @@ export function renderStart(r) {
}}
>
Jump
</Button>
</div>
)
</Button> */}
</div>
);
}
const renderXHRText = () => (
@ -243,39 +238,45 @@ export default class NetworkContent extends React.PureComponent {
iconPosition="left"
name="filter"
onChange={this.onFilterChange}
height={28}
/>
</BottomBlock.Header>
<BottomBlock.Content>
<InfoLine>
<InfoLine.Point label={filtered.length} value=" requests" />
<InfoLine.Point
label={formatBytes(transferredSize)}
value="transferred"
display={transferredSize > 0}
/>
<InfoLine.Point
label={formatBytes(resourcesSize)}
value="resources"
display={resourcesSize > 0}
/>
<InfoLine.Point
label={formatMs(domBuildingTime)}
value="DOM Building Time"
display={domBuildingTime != null}
/>
<InfoLine.Point
label={domContentLoadedTime && formatMs(domContentLoadedTime.value)}
value="DOMContentLoaded"
display={domContentLoadedTime != null}
dotColor={DOM_LOADED_TIME_COLOR}
/>
<InfoLine.Point
label={loadTime && formatMs(loadTime.value)}
value="Load"
display={loadTime != null}
dotColor={LOAD_TIME_COLOR}
/>
</InfoLine>
<div className="flex items-center justify-between px-4">
<div>
<Toggler checked={true} name="test" onChange={() => {}} label="4xx-5xx Only" />
</div>
<InfoLine>
<InfoLine.Point label={filtered.length} value=" requests" />
<InfoLine.Point
label={formatBytes(transferredSize)}
value="transferred"
display={transferredSize > 0}
/>
<InfoLine.Point
label={formatBytes(resourcesSize)}
value="resources"
display={resourcesSize > 0}
/>
<InfoLine.Point
label={formatMs(domBuildingTime)}
value="DOM Building Time"
display={domBuildingTime != null}
/>
<InfoLine.Point
label={domContentLoadedTime && formatMs(domContentLoadedTime.value)}
value="DOMContentLoaded"
display={domContentLoadedTime != null}
dotColor={DOM_LOADED_TIME_COLOR}
/>
<InfoLine.Point
label={loadTime && formatMs(loadTime.value)}
value="Load"
display={loadTime != null}
dotColor={LOAD_TIME_COLOR}
/>
</InfoLine>
</div>
<NoContent
title={
<div className="capitalize flex items-center mt-16">
@ -296,11 +297,11 @@ export default class NetworkContent extends React.PureComponent {
activeIndex={lastIndex}
>
{[
{
label: 'Start',
width: 120,
render: renderStart,
},
// {
// label: 'Start',
// width: 120,
// render: renderStart,
// },
{
label: 'Status',
dataKey: 'status',

View file

@ -2,13 +2,13 @@ import React from 'react';
import { connect } from 'react-redux';
import { Controls as PlayerControls, connectPlayer } from 'Player';
import {
AreaChart,
AreaChart,
Area,
ComposedChart,
Line,
XAxis,
XAxis,
YAxis,
Tooltip,
Tooltip,
ResponsiveContainer,
ReferenceLine,
CartesianGrid,
@ -23,40 +23,35 @@ import stl from './performance.module.css';
import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
const CPU_VISUAL_OFFSET = 10;
const FPS_COLOR = '#C5E5E7';
const FPS_STROKE_COLOR = "#92C7CA";
const FPS_LOW_COLOR = "pink";
const FPS_VERY_LOW_COLOR = "red";
const CPU_COLOR = "#A8D1DE";
const CPU_STROKE_COLOR = "#69A5B8";
const FPS_STROKE_COLOR = '#92C7CA';
const FPS_LOW_COLOR = 'pink';
const FPS_VERY_LOW_COLOR = 'red';
const CPU_COLOR = '#A8D1DE';
const CPU_STROKE_COLOR = '#69A5B8';
const USED_HEAP_COLOR = '#A9ABDC';
const USED_HEAP_STROKE_COLOR = "#8588CF";
const USED_HEAP_STROKE_COLOR = '#8588CF';
const TOTAL_HEAP_STROKE_COLOR = '#4A4EB7';
const NODES_COUNT_COLOR = "#C6A9DC";
const NODES_COUNT_STROKE_COLOR = "#7360AC";
const HIDDEN_SCREEN_COLOR = "#CCC";
const NODES_COUNT_COLOR = '#C6A9DC';
const NODES_COUNT_STROKE_COLOR = '#7360AC';
const HIDDEN_SCREEN_COLOR = '#CCC';
const CURSOR_COLOR = "#394EFF";
const CURSOR_COLOR = '#394EFF';
const Gradient = ({ color, id }) => (
<linearGradient id={ id } x1="-1" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={ color } stopOpacity={ 0.7 } />
<stop offset="95%" stopColor={ color } stopOpacity={ 0.2 } />
<linearGradient id={id} x1="-1" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={color} stopOpacity={0.7} />
<stop offset="95%" stopColor={color} stopOpacity={0.2} />
</linearGradient>
);
const TOTAL_HEAP = "Allocated Heap";
const USED_HEAP = "JS Heap";
const FPS = "Framerate";
const CPU = "CPU Load";
const NODES_COUNT = "Nodes Сount";
const TOTAL_HEAP = 'Allocated Heap';
const USED_HEAP = 'JS Heap';
const FPS = 'Framerate';
const CPU = 'CPU Load';
const NODES_COUNT = 'Nodes Сount';
const FPSTooltip = ({ active, payload }) => {
if (!active || !payload || payload.length < 3) {
@ -64,8 +59,8 @@ const FPSTooltip = ({ active, payload }) => {
}
if (payload[0].value === null) {
return (
<div className={ stl.tooltipWrapper } style={{ color: HIDDEN_SCREEN_COLOR }}>
{"Page is not active. User switched the tab or hid the window."}
<div className={stl.tooltipWrapper} style={{ color: HIDDEN_SCREEN_COLOR }}>
{'Page is not active. User switched the tab or hid the window.'}
</div>
);
}
@ -79,9 +74,9 @@ const FPSTooltip = ({ active, payload }) => {
}
return (
<div className={ stl.tooltipWrapper } style={ style }>
<span className="font-medium">{`${ FPS }: `}</span>
{ Math.trunc(payload[0].value) }
<div className={stl.tooltipWrapper} style={style}>
<span className="font-medium">{`${FPS}: `}</span>
{Math.trunc(payload[0].value)}
</div>
);
};
@ -91,47 +86,47 @@ const CPUTooltip = ({ active, payload }) => {
return null;
}
return (
<div className={ stl.tooltipWrapper } >
<span className="font-medium">{`${ CPU }: `}</span>
{ payload[0].value - CPU_VISUAL_OFFSET }
{"%"}
<div className={stl.tooltipWrapper}>
<span className="font-medium">{`${CPU}: `}</span>
{payload[0].value - CPU_VISUAL_OFFSET}
{'%'}
</div>
);
};
const HeapTooltip = ({ active, payload}) => {
const HeapTooltip = ({ active, payload }) => {
if (!active || payload.length < 2) return null;
return (
<div className={ stl.tooltipWrapper } >
<div className={stl.tooltipWrapper}>
<p>
<span className="font-medium">{`${ TOTAL_HEAP }: `}</span>
{ formatBytes(payload[0].value)}
<span className="font-medium">{`${TOTAL_HEAP}: `}</span>
{formatBytes(payload[0].value)}
</p>
<p>
<span className="font-medium">{`${ USED_HEAP }: `}</span>
{ formatBytes(payload[1].value)}
<span className="font-medium">{`${USED_HEAP}: `}</span>
{formatBytes(payload[1].value)}
</p>
</div>
);
}
};
const NodesCountTooltip = ({ active, payload} ) => {
const NodesCountTooltip = ({ active, payload }) => {
if (!active || !payload || payload.length === 0) return null;
return (
<div className={ stl.tooltipWrapper } >
<div className={stl.tooltipWrapper}>
<p>
<span className="font-medium">{`${ NODES_COUNT }: `}</span>
{ payload[0].value }
<span className="font-medium">{`${NODES_COUNT}: `}</span>
{payload[0].value}
</p>
</div>
);
}
};
const TICKS_COUNT = 10;
function generateTicks(data: Array<Timed>): Array<number> {
if (data.length === 0) return [];
const minTime = data[0].time;
const maxTime = data[data.length-1].time;
const maxTime = data[data.length - 1].time;
const ticks = [];
const tickGap = (maxTime - minTime) / (TICKS_COUNT + 1);
@ -159,8 +154,9 @@ function addFpsMetadata(data) {
} else if (point.fps < LOW_FPS) {
fpsLowMarker = LOW_FPS_MARKER_VALUE;
}
}
if (point.fps == null ||
}
if (
point.fps == null ||
(i > 0 && data[i - 1].fps == null) //||
//(i < data.length-1 && data[i + 1].fps == null)
) {
@ -174,17 +170,17 @@ function addFpsMetadata(data) {
fpsLowMarker,
fpsVeryLowMarker,
hiddenScreenMarker,
}
};
});
}
@connect(state => ({
userDeviceHeapSize: state.getIn([ "sessions", "current", "userDeviceHeapSize" ]),
userDeviceMemorySize: state.getIn([ "sessions", "current", "userDeviceMemorySize" ]),
@connect((state) => ({
userDeviceHeapSize: state.getIn(['sessions', 'current', 'userDeviceHeapSize']),
userDeviceMemorySize: state.getIn(['sessions', 'current', 'userDeviceMemorySize']),
}))
export default class Performance extends React.PureComponent {
_timeTicks = generateTicks(this.props.performanceChartData)
_data = addFpsMetadata(this.props.performanceChartData)
_timeTicks = generateTicks(this.props.performanceChartData);
_data = addFpsMetadata(this.props.performanceChartData);
// state = {
// totalHeap: false,
// usedHeap: true,
@ -197,7 +193,7 @@ export default class Performance extends React.PureComponent {
if (!!point) {
PlayerControls.jump(point.time);
}
}
};
onChartClick = (e) => {
if (e === null) return;
@ -206,10 +202,10 @@ export default class Performance extends React.PureComponent {
if (!!point) {
PlayerControls.jump(point.time);
}
}
};
render() {
const {
const {
userDeviceHeapSize,
userDeviceMemorySize,
connType,
@ -218,19 +214,19 @@ export default class Performance extends React.PureComponent {
avaliability = {},
} = this.props;
const { fps, cpu, heap, nodes } = avaliability;
const avaliableCount = [ fps, cpu, heap, nodes ].reduce((c, av) => av ? c + 1 : c, 0);
const height = avaliableCount === 0 ? "0" : `${100 / avaliableCount}%`;
const avaliableCount = [fps, cpu, heap, nodes].reduce((c, av) => (av ? c + 1 : c), 0);
const height = avaliableCount === 0 ? '0' : `${100 / avaliableCount}%`;
return (
<BottomBlock>
<BottomBlock.Header>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Performance</span>
<div className="flex items-center w-full">
<div className="font-semibold color-gray-medium mr-auto">Performance</div>
<InfoLine>
<InfoLine.Point
label="Device Heap Size"
value={ formatBytes(userDeviceHeapSize) }
display={ userDeviceHeapSize != null }
value={formatBytes(userDeviceHeapSize)}
display={true}
/>
{/* <InfoLine.Point */}
{/* label="Device Memory Size" */}
@ -238,55 +234,52 @@ export default class Performance extends React.PureComponent {
{/* /> */}
<InfoLine.Point
label="Connection Type"
value={ connType }
display={ connType != null && connType !== "unknown" }
value={connType}
display={connType != null && connType !== 'unknown'}
/>
<InfoLine.Point
label="Connection Speed"
value={ connBandwidth >= 1000
? `${ connBandwidth / 1000 } Mbps`
: `${ connBandwidth } Kbps`
value={
connBandwidth >= 1000 ? `${connBandwidth / 1000} Mbps` : `${connBandwidth} Kbps`
}
display={ connBandwidth != null }
display={connBandwidth != null}
/>
</InfoLine>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
{ fps &&
<ResponsiveContainer height={ height }>
{fps && (
<ResponsiveContainer height={height}>
<AreaChart
onClick={ this.onChartClick }
onClick={this.onChartClick}
data={this._data}
syncId="s"
margin={{
top: 0, right: 0, left: 0, bottom: 0,
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<Gradient id="fpsGradient" color={ FPS_COLOR } />
<Gradient id="fpsGradient" color={FPS_COLOR} />
</defs>
{/* <CartesianGrid strokeDasharray="1 1" stroke="#ddd" horizontal={ false } /> */}
{/* <CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" /> */}
<XAxis
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={ false }
tickFormatter={ durationFromMsFormatted }
tick={{ fontSize: "12px" }}
tickLine={false}
tickFormatter={durationFromMsFormatted}
tick={{ fontSize: '12px', fill: '#333' }}
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="FPS" position="insideTopRight" className="fill-gray-medium" />
<Label value="FPS" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis
axisLine={ false }
tick={ false }
mirror
domain={[0, 85]}
/>
<YAxis axisLine={false} tick={false} mirror domain={[0, 85]} />
{/* <YAxis */}
{/* yAxisId="r" */}
{/* axisLine={ false } */}
@ -295,41 +288,41 @@ export default class Performance extends React.PureComponent {
{/* domain={[0, 120]} */}
{/* orientation="right" */}
{/* /> */}
<Area
dataKey="fps"
type="stepBefore"
<Area
dataKey="fps"
type="stepBefore"
stroke={FPS_STROKE_COLOR}
fill="url(#fpsGradient)"
dot={false}
activeDot={{
activeDot={{
onClick: this.onDotClick,
style: { cursor: "pointer" },
}}
isAnimationActive={ false }
/>
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<Area
dataKey="fpsLowMarker"
dataKey="fpsLowMarker"
type="stepBefore"
stroke="none"
fill={ FPS_LOW_COLOR }
fill={FPS_LOW_COLOR}
activeDot={false}
isAnimationActive={ false }
isAnimationActive={false}
/>
<Area
<Area
dataKey="fpsVeryLowMarker"
type="stepBefore"
stroke="none"
fill={ FPS_VERY_LOW_COLOR }
fill={FPS_VERY_LOW_COLOR}
activeDot={false}
isAnimationActive={ false }
isAnimationActive={false}
/>
<Area
dataKey="hiddenScreenMarker"
type="stepBefore"
stroke="none"
fill={ HIDDEN_SCREEN_COLOR }
fill={HIDDEN_SCREEN_COLOR}
activeDot={false}
isAnimationActive={ false }
isAnimationActive={false}
/>
{/* <Area */}
{/* yAxisId="r" */}
@ -346,101 +339,95 @@ export default class Performance extends React.PureComponent {
{/* isAnimationActive={ false } */}
{/* /> */}
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip
content={FPSTooltip}
filterNull={ false }
/>
<Tooltip content={FPSTooltip} filterNull={false} />
</AreaChart>
</ResponsiveContainer>
}
{ cpu &&
<ResponsiveContainer height={ height }>
)}
{cpu && (
<ResponsiveContainer height={height}>
<AreaChart
onClick={ this.onChartClick }
onClick={this.onChartClick}
data={this._data}
syncId="s"
margin={{
top: 0, right: 0, left: 0, bottom: 0,
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<Gradient id="cpuGradient" color={ CPU_COLOR } />
<Gradient id="cpuGradient" color={CPU_COLOR} />
</defs>
{/* <CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" /> */}
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={()=> ""}
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="CPU" position="insideTopRight" className="fill-gray-medium" />
</XAxis>
<YAxis
axisLine={ false }
tick={ false }
mirror
domain={[ 0, 120]}
orientation="right"
/>
<Area
dataKey="cpu"
type="monotone"
stroke={CPU_STROKE_COLOR}
// fill="none"
fill="url(#cpuGradient)"
dot={false}
activeDot={{
onClick: this.onDotClick,
style: { cursor: "pointer" },
}}
isAnimationActive={ false }
/>
<Area
dataKey="hiddenScreenMarker"
type="stepBefore"
stroke="none"
fill={ HIDDEN_SCREEN_COLOR }
activeDot={false}
isAnimationActive={ false }
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip
content={CPUTooltip}
filterNull={ false }
/>
</AreaChart>
</ResponsiveContainer>
}
{ heap &&
<ResponsiveContainer height={ height }>
<ComposedChart
onClick={ this.onChartClick }
data={this._data}
margin={{
top: 0, right: 0, left: 0, bottom: 0,
}}
syncId="s"
>
<defs>
<Gradient id="usedHeapGradient" color={ USED_HEAP_COLOR } />
</defs>
{/* <CartesianGrid strokeDasharray="1 1" stroke="#ddd" horizontal={ false } /> */}
<XAxis
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={()=> ""} // tick={false} + this._timeTicks to cartesian array
tickFormatter={() => ''}
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="HEAP" position="insideTopRight" className="fill-gray-medium" />
<Label value="CPU" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis axisLine={false} tick={false} mirror domain={[0, 120]} orientation="right" />
<Area
dataKey="cpu"
type="monotone"
stroke={CPU_STROKE_COLOR}
// fill="none"
fill="url(#cpuGradient)"
dot={false}
activeDot={{
onClick: this.onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<Area
dataKey="hiddenScreenMarker"
type="stepBefore"
stroke="none"
fill={HIDDEN_SCREEN_COLOR}
activeDot={false}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={CPUTooltip} filterNull={false} />
</AreaChart>
</ResponsiveContainer>
)}
{heap && (
<ResponsiveContainer height={height}>
<ComposedChart
onClick={this.onChartClick}
data={this._data}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
syncId="s"
>
<defs>
<Gradient id="usedHeapGradient" color={USED_HEAP_COLOR} />
</defs>
{/* <CartesianGrid strokeDasharray="1 1" stroke="#ddd" horizontal={ false } /> */}
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={() => ''} // tick={false} + this._timeTicks to cartesian array
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="HEAP" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis
axisLine={false}
@ -448,19 +435,19 @@ export default class Performance extends React.PureComponent {
mirror
// Hack to keep only end tick
minTickGap={Number.MAX_SAFE_INTEGER}
domain={[0, max => max*1.2]}
domain={[0, (max) => max * 1.2]}
/>
<Line
<Line
type="monotone"
dataKey="totalHeap"
// fill="url(#totalHeapGradient)"
stroke={TOTAL_HEAP_STROKE_COLOR}
dot={false}
activeDot={{
activeDot={{
onClick: this.onDotClick,
style: { cursor: "pointer" },
style: { cursor: 'pointer' },
}}
isAnimationActive={ false }
isAnimationActive={false}
/>
<Area
dataKey="usedHeap"
@ -468,81 +455,78 @@ export default class Performance extends React.PureComponent {
fill="url(#usedHeapGradient)"
stroke={USED_HEAP_STROKE_COLOR}
dot={false}
activeDot={{
activeDot={{
onClick: this.onDotClick,
style: { cursor: "pointer" },
}}
isAnimationActive={ false }
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip
content={HeapTooltip}
filterNull={ false }
/>
<Tooltip content={HeapTooltip} filterNull={false} />
</ComposedChart>
</ResponsiveContainer>
}
{ nodes &&
<ResponsiveContainer height={ height }>
)}
{nodes && (
<ResponsiveContainer height={height}>
<AreaChart
onClick={ this.onChartClick }
onClick={this.onChartClick}
data={this._data}
syncId="s"
margin={{
top: 0, right: 0, left: 0, bottom: 0,
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<Gradient id="nodesGradient" color={ NODES_COUNT_COLOR } />
<Gradient id="nodesGradient" color={NODES_COUNT_COLOR} />
</defs>
{/* <CartesianGrid strokeDasharray="1 1" stroke="#ddd" horizontal={ false } /> */}
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={()=> ""}
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="NODES" position="insideTopRight" className="fill-gray-medium" />
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={() => ''}
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="NODES" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis
axisLine={ false }
tick={ false }
axisLine={false}
tick={false}
mirror
orientation="right"
domain={[0, max => max*1.2]}
domain={[0, (max) => max * 1.2]}
/>
<Area
dataKey="nodesCount"
type="monotone"
<Area
dataKey="nodesCount"
type="monotone"
stroke={NODES_COUNT_STROKE_COLOR}
// fill="none"
fill="url(#nodesGradient)"
dot={false}
activeDot={{
activeDot={{
onClick: this.onDotClick,
style: { cursor: "pointer" },
}}
isAnimationActive={ false }
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip
content={NodesCountTooltip}
filterNull={ false }
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={NodesCountTooltip} filterNull={false} />
</AreaChart>
</ResponsiveContainer>
}
)}
</BottomBlock.Content>
</BottomBlock>
);
}
}
export const ConnectedPerformance = connectPlayer(state => ({
export const ConnectedPerformance = connectPlayer((state) => ({
performanceChartTime: state.performanceChartTime,
performanceChartData: state.performanceChartData,
connType: state.connType,

View file

@ -3,4 +3,5 @@
padding: 2px 5px;
border-radius: 3px;
border: 1px solid #ccc;
color: $gray-dark !important;
}

View file

@ -42,6 +42,7 @@ import Overlay from './Overlay';
import stl from './player.module.css';
import { updateLastPlayedSession } from 'Duck/sessions';
import OverviewPanel from '../OverviewPanel';
import NetworkPanel from 'Shared/DevTools/NetworkPanel/NetworkPanel';
@connectPlayer(state => ({
live: state.live,
@ -112,7 +113,8 @@ export default class Player extends React.PureComponent {
<Console />
}
{ bottomBlock === NETWORK &&
<Network />
// <Network />
<NetworkPanel />
}
{ bottomBlock === STACKEVENTS &&
<StackEvents />

View file

@ -9,7 +9,7 @@ $offset: 10px;
.headers {
box-shadow: 0 1px 2px 0 $gray-light;
background-color: $gray-lightest;
color: $gray-medium;
color: $gray-darkest;
font-size: 12px;
overflow-x: hidden;
white-space: nowrap;
@ -47,6 +47,10 @@ $offset: 10px;
.row {
display: flex;
padding: 0 $offset;
&:hover {
background-color: $active-blue;
}
/*align-items: center;
cursor: pointer;
*/

View file

@ -0,0 +1,18 @@
import React from 'react';
import cn from 'classnames';
import stl from './bottomBlock.module.css';
const BottomBlock = ({
children = null,
className = '',
additionalHeight = 0,
...props
}) => (
<div className={ cn(stl.wrapper, "flex flex-col mb-2") } { ...props } >
{ children }
</div>
);
BottomBlock.displayName = 'BottomBlock';
export default BottomBlock;

View file

@ -0,0 +1,17 @@
import React from 'react';
import cn from 'classnames';
import stl from './content.module.css';
const Content = ({
children,
className,
...props
}) => (
<div className={ cn(className, stl.content) } { ...props } >
{ children }
</div>
);
Content.displayName = 'Content';
export default Content;

View file

@ -0,0 +1,26 @@
import React from 'react';
import { connect } from 'react-redux';
import cn from 'classnames';
import { closeBottomBlock } from 'Duck/components/player';
import { Input, CloseButton } from 'UI';
import stl from './header.module.css';
const Header = ({
children,
className,
closeBottomBlock,
onFilterChange,
showClose = true,
...props
}) => (
<div className={ cn("relative border-r border-l py-1", stl.header) } >
<div className={ cn("w-full h-full flex justify-between items-center", className) } >
<div className="w-full flex items-center justify-between">{ children }</div>
{ showClose && <CloseButton onClick={ closeBottomBlock } size="18" className="ml-2" /> }
</div>
</div>
);
Header.displayName = 'Header';
export default connect(null, { closeBottomBlock })(Header);

View file

@ -0,0 +1,20 @@
import React from 'react';
import cn from 'classnames';
import cls from './infoLine.module.css';
const InfoLine = ({ children }) => (
<div className={ cls.info }>
{ children }
</div>
)
const Point = ({ label = '', value = '', display=true, color, dotColor }) => display
? <div className={ cls.infoPoint } style={{ color }}>
{ dotColor != null && <div className={ cn(cls.dot, `bg-${dotColor}`) } /> }
<span className={cls.label}>{ `${label}` }</span> { value }
</div>
: null;
InfoLine.Point = Point;
export default InfoLine;

View file

@ -0,0 +1,9 @@
.wrapper {
background: $white;
/* padding-right: 10px; */
/* border: solid thin $gray-light; */
height: 300px;
border-top: thin dashed #cccccc;
}

View file

@ -0,0 +1,3 @@
.content {
height: 86%;
}

View file

@ -0,0 +1,6 @@
.header {
padding: 0 10px;
height: 40px;
border-bottom: 1px solid $gray-light;
}

View file

@ -0,0 +1,8 @@
import BottomBlock from './BottomBlock';
import Header from './Header';
import Content from './Content';
BottomBlock.Header = Header;
BottomBlock.Content = Content;
export default BottomBlock;

View file

@ -0,0 +1,31 @@
.info {
padding-left: 10px;
height: 36px;
display: flex;
align-items: center;
& >.infoPoint {
font-size: 14px;
display: flex;
align-items: center;
&:not(:last-child):after {
content: '';
margin: 0 12px;
height: 30px;
border-right: 1px solid $gray-light-shade;
}
& .label {
font-weight: 500;
margin-right: 6px;
}
}
}
.dot {
width: 10px;
height: 10px;
border-radius: 5px;
margin-right: 5px;
}

View file

@ -0,0 +1,9 @@
// import { NONE, CONSOLE, NETWORK, STACKEVENTS, REDUX_STATE, PROFILER, PERFORMANCE, GRAPHQL } from 'Duck/components/player';
//
//
// export default {
// [NONE]: {
// Component: null,
//
// }
// }

View file

@ -0,0 +1,351 @@
import React, { useState } from 'react';
import cn from 'classnames';
// import { connectPlayer } from 'Player';
import { QuestionMarkHint, Popup, Tabs, Input, NoContent, Icon, Toggler, Button } from 'UI';
import { getRE } from 'App/utils';
import { TYPES } from 'Types/session/resource';
import { formatBytes } from 'App/utils';
import { formatMs } from 'App/date';
import TimeTable from '../TimeTable';
import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
// import stl from './network.module.css';
import { Duration } from 'luxon';
import { connectPlayer, jump, pause } from 'Player';
import { useModal } from 'App/components/Modal';
import FetchDetailsModal from 'Shared/FetchDetailsModal';
const ALL = 'ALL';
const XHR = 'xhr';
const JS = 'js';
const CSS = 'css';
const IMG = 'img';
const MEDIA = 'media';
const OTHER = 'other';
const TAB_TO_TYPE_MAP: any = {
[XHR]: TYPES.XHR,
[JS]: TYPES.JS,
[CSS]: TYPES.CSS,
[IMG]: TYPES.IMG,
[MEDIA]: TYPES.MEDIA,
[OTHER]: TYPES.OTHER,
};
const TABS: any = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER].map((tab) => ({
text: tab,
key: tab,
}));
const DOM_LOADED_TIME_COLOR = 'teal';
const LOAD_TIME_COLOR = 'red';
export function renderType(r) {
return (
<Popup style={{ width: '100%' }} content={<div>{r.type}</div>}>
<div>{r.type}</div>
</Popup>
);
}
export function renderName(r: any) {
return (
<Popup style={{ width: '100%' }} content={<div>{r.url}</div>}>
<div>{r.name}</div>
</Popup>
);
}
export function renderStart(r: any) {
return (
<div className="flex justify-between items-center grow-0 w-full">
<span>{Duration.fromMillis(r.time).toFormat('mm:ss.SSS')}</span>
</div>
);
}
const renderXHRText = () => (
<span className="flex items-center">
{XHR}
<QuestionMarkHint
onHover={true}
content={
<>
Use our{' '}
<a
className="color-teal underline"
target="_blank"
href="https://docs.openreplay.com/plugins/fetch"
>
Fetch plugin
</a>
{' to capture HTTP requests and responses, including status codes and bodies.'} <br />
We also provide{' '}
<a
className="color-teal underline"
target="_blank"
href="https://docs.openreplay.com/plugins/graphql"
>
support for GraphQL
</a>
{' for easy debugging of your queries.'}
</>
}
className="ml-1"
/>
</span>
);
function renderSize(r: any) {
if (r.responseBodySize) return formatBytes(r.responseBodySize);
let triggerText;
let content;
if (r.decodedBodySize == null) {
triggerText = 'x';
content = 'Not captured';
} else {
const headerSize = r.headerSize || 0;
const encodedSize = r.encodedBodySize || 0;
const transferred = headerSize + encodedSize;
const showTransferred = r.headerSize != null;
triggerText = formatBytes(r.decodedBodySize);
content = (
<ul>
{showTransferred && (
<li>{`${formatBytes(r.encodedBodySize + headerSize)} transfered over network`}</li>
)}
<li>{`Resource size: ${formatBytes(r.decodedBodySize)} `}</li>
</ul>
);
}
return (
<Popup style={{ width: '100%' }} content={content}>
<div>{triggerText}</div>
</Popup>
);
}
export function renderDuration(r: any) {
if (!r.success) return 'x';
const text = `${Math.floor(r.duration)}ms`;
if (!r.isRed() && !r.isYellow()) return text;
let tooltipText;
let className = 'w-full h-full flex items-center ';
if (r.isYellow()) {
tooltipText = 'Slower than average';
className += 'warn color-orange';
} else {
tooltipText = 'Much slower than average';
className += 'error color-red';
}
return (
<Popup style={{ width: '100%' }} content={tooltipText}>
<div> {text} </div>
</Popup>
);
}
interface Props {
location: any;
resources: any;
domContentLoadedTime: any;
loadTime: any;
playing: boolean;
domBuildingTime: any;
fetchPresented: boolean;
listNow: any;
currentIndex: any;
time: any;
}
function NetworkPanel(props: Props) {
const {
resources,
time,
currentIndex,
domContentLoadedTime,
loadTime,
playing,
domBuildingTime,
fetchPresented,
listNow,
} = props;
const { showModal, hideModal } = useModal();
const [activeTab, setActiveTab] = useState(ALL);
const [filter, setFilter] = useState('');
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
const onTabClick = (activeTab: any) => setActiveTab(activeTab);
const onFilterChange = ({ target: { value } }: any) => setFilter(value);
const additionalHeight = 0;
const resourcesSize = resources.reduce(
(sum: any, { decodedBodySize }: any) => sum + (decodedBodySize || 0),
0
);
const transferredSize = resources.reduce(
(sum: any, { headerSize, encodedBodySize }: any) =>
sum + (headerSize || 0) + (encodedBodySize || 0),
0
);
const filterRE = getRE(filter, 'i');
let filtered = resources.filter(
({ type, name }: any) =>
filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab])
);
const lastIndex = currentIndex || filtered.filter((item: any) => item.time <= time).length - 1;
const referenceLines = [];
if (domContentLoadedTime != null) {
referenceLines.push({
time: domContentLoadedTime.time,
color: DOM_LOADED_TIME_COLOR,
});
}
if (loadTime != null) {
referenceLines.push({
time: loadTime.time,
color: LOAD_TIME_COLOR,
});
}
const onRowClick = (row: any) => {
showModal(<FetchDetailsModal resource={row} />, { right: true })
}
return (
<React.Fragment>
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }} className="border">
<BottomBlock.Header>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Network</span>
<Tabs
className="uppercase"
tabs={TABS}
active={activeTab}
onClick={onTabClick}
border={false}
/>
</div>
<Input
className="input-small"
placeholder="Filter by name"
icon="search"
iconPosition="left"
name="filter"
onChange={onFilterChange}
height={28}
/>
</BottomBlock.Header>
<BottomBlock.Content>
<div className="flex items-center justify-between px-4">
<div>
<Toggler checked={showOnlyErrors} name="test" onChange={() => setShowOnlyErrors(!showOnlyErrors)} label="4xx-5xx Only" />
</div>
<InfoLine>
<InfoLine.Point label={filtered.length} value=" requests" />
<InfoLine.Point
label={formatBytes(transferredSize)}
value="transferred"
display={transferredSize > 0}
/>
<InfoLine.Point
label={formatBytes(resourcesSize)}
value="resources"
display={resourcesSize > 0}
/>
<InfoLine.Point
label={formatMs(domBuildingTime)}
value="DOM Building Time"
display={domBuildingTime != null}
/>
<InfoLine.Point
label={domContentLoadedTime && formatMs(domContentLoadedTime.value)}
value="DOMContentLoaded"
display={domContentLoadedTime != null}
dotColor={DOM_LOADED_TIME_COLOR}
/>
<InfoLine.Point
label={loadTime && formatMs(loadTime.value)}
value="Load"
display={loadTime != null}
dotColor={LOAD_TIME_COLOR}
/>
</InfoLine>
</div>
<NoContent
title={
<div className="capitalize flex items-center mt-16">
<Icon name="info-circle" className="mr-2" size="18" />
No Data
</div>
}
size="small"
show={filtered.length === 0}
>
<TimeTable
rows={filtered}
referenceLines={referenceLines}
renderPopup
// navigation
onRowClick={onRowClick}
additionalHeight={additionalHeight}
activeIndex={lastIndex}
>
{[
// {
// label: 'Start',
// width: 120,
// render: renderStart,
// },
{
label: 'Status',
dataKey: 'status',
width: 70,
},
{
label: 'Type',
dataKey: 'type',
width: 90,
render: renderType,
},
{
label: 'Name',
width: 240,
render: renderName,
},
{
label: 'Size',
width: 60,
render: renderSize,
},
{
label: 'Time',
width: 80,
render: renderDuration,
},
]}
</TimeTable>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</React.Fragment>
);
}
export default connectPlayer((state: any) => ({
location: state.location,
resources: state.resourceList,
domContentLoadedTime: state.domContentLoadedTime,
loadTime: state.loadTime,
// time: state.time,
playing: state.playing,
domBuildingTime: state.domBuildingTime,
fetchPresented: state.fetchList.length > 0,
listNow: state.resourceListNow,
}))(NetworkPanel);

View file

@ -0,0 +1,96 @@
import { Popup } from 'UI';
import { percentOf } from 'App/utils';
import styles from './barRow.module.css'
import tableStyles from './timeTable.module.css';
import React from 'react';
const formatTime = time => time < 1000 ? `${time.toFixed(2)}ms` : `${time / 1000}s`;
interface Props {
resource: {
time: number
ttfb?: number
duration?: number
key: string
}
popup?: boolean
timestart: number
timewidth: number
}
// TODO: If request has no duration, set duration to 0.2s. Enforce existence of duration in the future.
const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = false, timestart = 0, timewidth }: Props) => {
const timeOffset = time - timestart;
ttfb = ttfb || 0;
const trigger = (
<div
className={styles.barWrapper}
style={{
left: `${percentOf(timeOffset, timewidth)}%`,
right: `${100 - percentOf(timeOffset + duration, timewidth)}%`,
minWidth: '5px'
}}
>
<div
className={styles.ttfbBar}
style={{
width: `${percentOf(ttfb, duration)}%`,
}}
/>
<div
className={styles.downloadBar}
style={{
width: `${percentOf(duration - ttfb, duration)}%`,
minWidth: '5px'
}}
/>
</div>
);
if (!popup) return <div key={key} className={tableStyles.row} > {trigger} </div>;
return (
<div key={key} className={tableStyles.row} >
<Popup
basic
content={
<React.Fragment>
{ttfb != null &&
<div className={styles.popupRow}>
<div className={styles.title}>{'Waiting (TTFB)'}</div>
<div className={styles.popupBarWrapper} >
<div
className={styles.ttfbBar}
style={{
left: 0,
width: `${percentOf(ttfb, duration)}%`,
}}
/>
</div>
<div className={styles.time} >{formatTime(ttfb)}</div>
</div>
}
<div className={styles.popupRow}>
<div className={styles.title} >{'Content Download'}</div>
<div className={styles.popupBarWrapper}>
<div
className={styles.downloadBar}
style={{
left: `${percentOf(ttfb, duration)}%`,
width: `${percentOf(duration - ttfb, duration)}%`,
}}
/>
</div>
<div className={styles.time}>{formatTime(duration - ttfb)}</div>
</div>
</React.Fragment>
}
size="mini"
position="top center"
/>
</div>
);
}
BarRow.displayName = "BarRow";
export default BarRow;

View file

@ -0,0 +1,318 @@
import React from 'react';
import { List, AutoSizer } from 'react-virtualized';
import cn from 'classnames';
import { Duration } from "luxon";
import { NoContent, IconButton, Button } from 'UI';
import { percentOf } from 'App/utils';
import BarRow from './BarRow';
import stl from './timeTable.module.css';
import autoscrollStl from '../autoscroll.module.css'; //aaa
type Timed = {
time: number;
};
type Durationed = {
duration: number;
};
type CanBeRed = {
//+isRed: boolean,
isRed: () => boolean;
};
interface Row extends Timed, Durationed, CanBeRed {
[key: string]: any, key: string
}
type Line = {
color: string; // Maybe use typescript?
hint?: string;
onClick?: any;
} & Timed;
type Column = {
label: string;
width: number;
dataKey?: string;
render?: (row: any) => void
referenceLines?: Array<Line>;
style?: React.CSSProperties;
} & RenderOrKey;
// type RenderOrKey = { // Disjoint?
// render: Row => React.Node
// } | {
// dataKey: string,
// }
type RenderOrKey =
| {
render?: (row: Row) => React.ReactNode;
key?: string;
}
| {
dataKey: string;
};
type Props = {
className?: string;
rows: Array<Row>;
children: Array<Column>;
tableHeight?: number
activeIndex?: number
renderPopup?: boolean
navigation?: boolean
referenceLines?: any[]
additionalHeight?: number
hoverable?: boolean
onRowClick?: (row: any, index: number) => void
};
type TimeLineInfo = {
timestart: number;
timewidth: number;
};
type State = TimeLineInfo & typeof initialState;
//const TABLE_HEIGHT = 195;
let _additionalHeight = 0;
const ROW_HEIGHT = 32;
//const VISIBLE_COUNT = Math.ceil(TABLE_HEIGHT/ROW_HEIGHT);
const TIME_SECTIONS_COUNT = 8;
const ZERO_TIMEWIDTH = 1000;
function formatTime(ms: number) {
if (ms < 0) return '';
if (ms < 1000) return Duration.fromMillis(ms).toFormat('0.SSS')
return Duration.fromMillis(ms).toFormat('mm:ss');
}
function computeTimeLine(rows: Array<Row>, firstVisibleRowIndex: number, visibleCount: number): TimeLineInfo {
const visibleRows = rows.slice(firstVisibleRowIndex, firstVisibleRowIndex + visibleCount + _additionalHeight);
let timestart = visibleRows.length > 0 ? Math.min(...visibleRows.map((r) => r.time)) : 0;
// TODO: GraphQL requests do not have a duration, so their timeline is borked. Assume a duration of 0.2s for every GraphQL request
const timeend = visibleRows.length > 0 ? Math.max(...visibleRows.map((r) => r.time + (r.duration ?? 200))) : 0;
let timewidth = timeend - timestart;
const offset = timewidth / 70;
if (timestart >= offset) {
timestart -= offset;
}
timewidth *= 1.5; // += offset;
if (timewidth === 0) {
timewidth = ZERO_TIMEWIDTH;
}
return {
timestart,
timewidth,
};
}
const initialState = {
firstVisibleRowIndex: 0,
};
export default class TimeTable extends React.PureComponent<Props, State> {
state = {
...computeTimeLine(this.props.rows, initialState.firstVisibleRowIndex, this.visibleCount),
...initialState,
};
get tableHeight() {
return this.props.tableHeight || 195;
}
get visibleCount() {
return Math.ceil(this.tableHeight / ROW_HEIGHT);
}
scroller = React.createRef<List>();
autoScroll = true;
componentDidMount() {
if (this.scroller.current) {
this.scroller.current.scrollToRow(this.props.activeIndex);
}
}
componentDidUpdate(prevProps: any, prevState: any) {
if (
prevState.firstVisibleRowIndex !== this.state.firstVisibleRowIndex ||
(this.props.rows.length <= this.visibleCount + _additionalHeight && prevProps.rows.length !== this.props.rows.length)
) {
this.setState({
...computeTimeLine(this.props.rows, this.state.firstVisibleRowIndex, this.visibleCount),
});
}
if (this.props.activeIndex && this.props.activeIndex >= 0 && prevProps.activeIndex !== this.props.activeIndex && this.scroller.current) {
this.scroller.current.scrollToRow(this.props.activeIndex);
}
}
onScroll = ({ scrollTop, scrollHeight, clientHeight }: { scrollTop: number; scrollHeight: number; clientHeight: number }): void => {
const firstVisibleRowIndex = Math.floor(scrollTop / ROW_HEIGHT + 0.33);
if (this.state.firstVisibleRowIndex !== firstVisibleRowIndex) {
this.autoScroll = scrollHeight - clientHeight - scrollTop < ROW_HEIGHT / 2;
this.setState({ firstVisibleRowIndex });
}
};
renderRow = ({ index, key, style: rowStyle }: any) => {
const { activeIndex } = this.props;
const { children: columns, rows, renderPopup, hoverable, onRowClick } = this.props;
const { timestart, timewidth } = this.state;
const row = rows[index];
return (
<div
style={rowStyle}
key={key}
className={cn('border-b border-color-gray-light-shade', stl.row, {
[stl.hoverable]: hoverable,
'error color-red': !!row.isRed && row.isRed(),
'cursor-pointer': typeof onRowClick === 'function',
[stl.activeRow]: activeIndex === index,
// [stl.inactiveRow]: !activeIndex || index > activeIndex,
})}
onClick={typeof onRowClick === 'function' ? () => onRowClick(row, index) : undefined}
id="table-row"
>
{columns.map(({ dataKey, render, width }) => (
<div className={stl.cell} style={{ width: `${width}px` }}>
{render ? render(row) : row[dataKey || ''] || <i className="color-gray-light">{'empty'}</i>}
</div>
))}
<div className={cn('relative flex-1 flex', stl.timeBarWrapper)}>
<BarRow resource={row} timestart={timestart} timewidth={timewidth} popup={renderPopup} />
</div>
</div>
);
};
onPrevClick = () => {
let prevRedIndex = -1;
for (let i = this.state.firstVisibleRowIndex - 1; i >= 0; i--) {
if (this.props.rows[i].isRed()) {
prevRedIndex = i;
break;
}
}
if (this.scroller.current != null) {
this.scroller.current.scrollToRow(prevRedIndex);
}
};
onNextClick = () => {
let prevRedIndex = -1;
for (let i = this.state.firstVisibleRowIndex + 1; i < this.props.rows.length; i++) {
if (this.props.rows[i].isRed()) {
prevRedIndex = i;
break;
}
}
if (this.scroller.current != null) {
this.scroller.current.scrollToRow(prevRedIndex);
}
};
render() {
const { className, rows, children: columns, navigation = false, referenceLines = [], additionalHeight = 0, activeIndex } = this.props;
const { timewidth, timestart } = this.state;
_additionalHeight = additionalHeight;
const sectionDuration = Math.round(timewidth / TIME_SECTIONS_COUNT);
const timeColumns: number[] = [];
if (timewidth > 0) {
for (let i = 0; i < TIME_SECTIONS_COUNT; i++) {
timeColumns.push(timestart + i * sectionDuration);
}
}
const visibleRefLines = referenceLines.filter(({ time }) => time > timestart && time < timestart + timewidth);
const columnsSumWidth = columns.reduce((sum, { width }) => sum + width, 0);
return (
<div className={cn(className, 'relative')}>
{navigation && (
<div className={cn(autoscrollStl.navButtons, 'flex items-center')}>
<Button
variant="text-primary"
icon="chevron-up"
tooltip={{
title: 'Previous Error',
delay: 0,
}}
onClick={this.onPrevClick}
/>
<Button
variant="text-primary"
icon="chevron-down"
tooltip={{
title: 'Next Error',
delay: 0,
}}
onClick={this.onNextClick}
/>
</div>
)}
<div className={stl.headers}>
<div className={stl.infoHeaders}>
{columns.map(({ label, width }) => (
<div className={stl.headerCell} style={{ width: `${width}px` }}>
{label}
</div>
))}
</div>
<div className={stl.waterfallHeaders}>
{timeColumns.map((time, i) => (
<div className={stl.timeCell} key={`tc-${i}`}>
{formatTime(time)}
</div>
))}
</div>
</div>
<NoContent size="small" show={rows.length === 0}>
<div className="relative">
<div className={stl.timePart} style={{ left: `${columnsSumWidth}px` }}>
{timeColumns.map((_, index) => (
<div key={`tc-${index}`} className={stl.timeCell} />
))}
{visibleRefLines.map(({ time, color, onClick }) => (
<div
className={cn(stl.refLine, `bg-${color}`)}
style={{
left: `${percentOf(time - timestart, timewidth)}%`,
cursor: typeof onClick === 'function' ? 'click' : 'auto',
}}
onClick={onClick}
/>
))}
</div>
<AutoSizer disableHeight>
{({ width }: { width: number }) => (
<List
ref={this.scroller}
className={stl.list}
height={this.tableHeight + additionalHeight}
width={width}
overscanRowCount={20}
rowCount={rows.length}
rowHeight={ROW_HEIGHT}
rowRenderer={this.renderRow}
onScroll={this.onScroll}
scrollToAlignment="start"
forceUpdateProp={timestart | timewidth | (activeIndex || 0)}
/>
)}
</AutoSizer>
</div>
</NoContent>
</div>
);
}
}

View file

@ -0,0 +1,45 @@
.barWrapper {
display: flex;
position: absolute;
top: 35%;
bottom: 35%;
border-radius: 3px;
overflow: hidden;
}
.downloadBar, .ttfbBar {
/* box-shadow: inset 0px 0px 0px 1px $teal; */
height: 100%;
box-sizing: border-box;
position: relative;
}
.ttfbBar {
background-color: rgba(175, 226, 221, 0.8);
}
.downloadBar {
background-color: rgba(133, 200, 192, 0.8);
}
.popupRow {
color: $gray-medium;
display: flex;
align-items: center;
padding: 2px 0;
font-size: 12px;
}
.title {
width: 105px;
}
.time {
width: 60px;
padding-left: 10px;
}
.popupBarWrapper {
width: 220px;
height: 15px;
border-radius: 3px;
overflow: hidden;
}

View file

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

View file

@ -0,0 +1,112 @@
$offset: 10px;
.timeCell {
border-left: solid thin rgba(0, 0, 0, 0.05);
}
.headers {
box-shadow: 0 1px 2px 0 $gray-light;
background-color: $gray-lightest;
color: $gray-darkest;
font-size: 12px;
overflow-x: hidden;
white-space: nowrap;
width: 100%;
display: flex;
padding: 0 $offset;
}
.infoHeaders {
text-transform: uppercase;
display: flex;
& .headerCell {
padding: 4px 2px;
}
}
.waterfallHeaders {
display: flex;
flex: 1;
& .timeCell {
flex: 1;
overflow: hidden;
padding: 4px 0;
}
}
.list {
/* TODO hide the scrollbar track */
&::-webkit-scrollbar {
width: 1px;
}
scrollbar-width: thin;
font-size: 12px;
font-family: 'Menlo', 'monaco', 'consolas', monospace;
}
.row {
display: flex;
padding: 0 $offset;
&:hover {
background-color: $active-blue;
}
/*align-items: center;
cursor: pointer;
*/
/* &:nth-child(even) {
background-color: $gray-lightest;
} */
/* & > div:first-child {
padding-left: 5px;
}*/
}
.cell {
height: 100%;
display: flex;
align-items: center;
overflow: hidden;
padding: 0 2px;
}
.hoverable {
transition: all 0.3s;
cursor: pointer;
&:hover {
background-color: $active-blue;
transition: all 0.2s;
color: $gray-dark;
}
}
.timeBarWrapper{
overflow: hidden;
}
.timePart {
position: absolute;
top: 0;
bottom: 0;
/*left:0;*/
right: 0;
display: flex;
margin: 0 $offset;
& .timeCell {
height: 100%;
flex: 1;
z-index: 1;
pointer-events: none;
}
& .refLine {
position: absolute;
height: 100%;
width: 1px;
z-index: 1;
}
}
.activeRow {
background-color: $teal-light;
}
.inactiveRow {
opacity: 0.5;
}

View file

@ -0,0 +1,12 @@
.navButtons {
position: absolute;
background: rgba(255, 255, 255, 0.5);
padding: 4px;
right: 24px;
top: 8px;
z-index: 1;
}

View file

@ -128,24 +128,60 @@ export default class FetchDetailsModal extends React.PureComponent {
}
render() {
const {
resource: { method, url, duration },
nextClick,
prevClick,
first = false,
last = false,
} = this.props;
const { resource, nextClick, prevClick, first = false, last = false } = this.props;
const { method, url, duration } = resource;
const { activeTab, tabs } = this.state;
const _duration = parseInt(duration)
console.log('_duration', _duration);
const _duration = parseInt(duration);
console.log('_duration', resource);
return (
<div className="bg-white p-5 h-screen overflow-y-auto" style={{ width: '500px' }}>
<h5 className="mb-2">{'URL'}</h5>
<div className={cn(stl.url, 'color-gray-darkest')}>{url}</div>
<h5 className="mb-2 text-2xl">Network Request</h5>
<div className="flex items-center py-1">
<div className="font-medium">Name</div>
<div className="rounded-lg bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{resource.name}
</div>
</div>
<div className="flex items-center py-1">
<div className="font-medium">Type</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{resource.type}
</div>
</div>
{method && (
<div className="flex items-center py-1">
<div className="font-medium">Request Method</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{resource.method}
</div>
</div>
)}
{resource.status && (
<div className="flex items-center py-1">
<div className="font-medium">Status</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip flex items-center">
{resource.status === '200' && <div className="w-4 h-4 bg-green rounded-full mr-2"></div>}
{resource.status}
</div>
</div>
)}
{!!_duration && (
<div className="flex items-center py-1">
<div className="font-medium">Time</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{_duration} ms
</div>
</div>
)}
{/* <div className={cn(stl.url, 'color-gray-darkest')}>{url}</div> */}
<div className="flex items-start mt-4">
{method && (
{/* {method && (
<div className="w-4/12">
<div className="font-medium mb-2">Method</div>
<div>{method}</div>
@ -156,7 +192,7 @@ export default class FetchDetailsModal extends React.PureComponent {
<div className="font-medium mb-2">Duration</div>
<div>{_duration } ms</div>
</div>
)}
)} */}
</div>
<div className="mt-6">

View file

@ -9,10 +9,11 @@ interface Props {
leadingButton?: React.ReactNode;
type?: string;
rows?: number;
height?: number;
[x: string]: any;
}
const Input = React.forwardRef((props: Props, ref: any) => {
const { className = '', leadingButton = '', wrapperClassName = '', icon = '', type = 'text', rows = 4, ...rest } = props;
const { height = 36, className = '', leadingButton = '', wrapperClassName = '', icon = '', type = 'text', rows = 4, ...rest } = props;
return (
<div className={cn({ relative: icon || leadingButton }, wrapperClassName)}>
{icon && <Icon name={icon} className="absolute top-0 bottom-0 my-auto ml-4" size="14" />}
@ -29,7 +30,7 @@ const Input = React.forwardRef((props: Props, ref: any) => {
<input
ref={ref}
type={type}
style={{ height: '36px' }}
style={{ height: `${height}px` }}
className={cn('p-2 border border-gray-light bg-white w-full rounded', className, { 'pl-10': icon })}
{...rest}
/>