From a6227c72655dc45b009b01dbff78b16dd0fd084b Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 11 Oct 2022 10:36:20 +0200 Subject: [PATCH] change(ui) - dev tools --- .../app/components/Session_/Autoscroll.tsx | 6 +- .../Session_/BottomBlock/infoLine.module.css | 2 +- .../Session_/Console/ConsoleContent.js | 11 +- .../Session_/Console/console.module.css | 3 + .../Session_/Network/NetworkContent.js | 99 ++-- .../Session_/Performance/Performance.tsx | 436 +++++++++--------- .../Performance/performance.module.css | 1 + .../app/components/Session_/Player/Player.js | 4 +- .../Session_/TimeTable/timeTable.module.css | 6 +- .../DevTools/BottomBlock/BottomBlock.js | 18 + .../shared/DevTools/BottomBlock/Content.js | 17 + .../shared/DevTools/BottomBlock/Header.js | 26 ++ .../shared/DevTools/BottomBlock/InfoLine.js | 20 + .../BottomBlock/bottomBlock.module.css | 9 + .../DevTools/BottomBlock/content.module.css | 3 + .../DevTools/BottomBlock/header.module.css | 6 + .../shared/DevTools/BottomBlock/index.js | 8 + .../DevTools/BottomBlock/infoLine.module.css | 31 ++ .../shared/DevTools/BottomBlock/tabs.js | 9 + .../DevTools/NetworkPanel/NetworkPanel.tsx | 351 ++++++++++++++ .../shared/DevTools/TimeTable/BarRow.tsx | 96 ++++ .../shared/DevTools/TimeTable/TimeTable.tsx | 318 +++++++++++++ .../DevTools/TimeTable/barRow.module.css | 45 ++ .../shared/DevTools/TimeTable/index.js | 1 + .../DevTools/TimeTable/timeTable.module.css | 112 +++++ .../shared/DevTools/autoscroll.module.css | 12 + .../FetchDetailsModal/FetchDetailsModal.js | 64 ++- frontend/app/components/ui/Input/Input.tsx | 5 +- 28 files changed, 1417 insertions(+), 302 deletions(-) create mode 100644 frontend/app/components/shared/DevTools/BottomBlock/BottomBlock.js create mode 100644 frontend/app/components/shared/DevTools/BottomBlock/Content.js create mode 100644 frontend/app/components/shared/DevTools/BottomBlock/Header.js create mode 100644 frontend/app/components/shared/DevTools/BottomBlock/InfoLine.js create mode 100644 frontend/app/components/shared/DevTools/BottomBlock/bottomBlock.module.css create mode 100644 frontend/app/components/shared/DevTools/BottomBlock/content.module.css create mode 100644 frontend/app/components/shared/DevTools/BottomBlock/header.module.css create mode 100644 frontend/app/components/shared/DevTools/BottomBlock/index.js create mode 100644 frontend/app/components/shared/DevTools/BottomBlock/infoLine.module.css create mode 100644 frontend/app/components/shared/DevTools/BottomBlock/tabs.js create mode 100644 frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx create mode 100644 frontend/app/components/shared/DevTools/TimeTable/BarRow.tsx create mode 100644 frontend/app/components/shared/DevTools/TimeTable/TimeTable.tsx create mode 100644 frontend/app/components/shared/DevTools/TimeTable/barRow.module.css create mode 100644 frontend/app/components/shared/DevTools/TimeTable/index.js create mode 100644 frontend/app/components/shared/DevTools/TimeTable/timeTable.module.css create mode 100644 frontend/app/components/shared/DevTools/autoscroll.module.css diff --git a/frontend/app/components/Session_/Autoscroll.tsx b/frontend/app/components/Session_/Autoscroll.tsx index 051f2024f..ad2e82e01 100644 --- a/frontend/app/components/Session_/Autoscroll.tsx +++ b/frontend/app/components/Session_/Autoscroll.tsx @@ -112,15 +112,15 @@ export default class Autoscroll extends React.PureComponent -
- {/* */} + {/*
+ {navigation && ( <> )} -
+
*/} ); diff --git a/frontend/app/components/Session_/BottomBlock/infoLine.module.css b/frontend/app/components/Session_/BottomBlock/infoLine.module.css index b6798d1bf..37e47f013 100644 --- a/frontend/app/components/Session_/BottomBlock/infoLine.module.css +++ b/frontend/app/components/Session_/BottomBlock/infoLine.module.css @@ -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 { diff --git a/frontend/app/components/Session_/Console/ConsoleContent.js b/frontend/app/components/Session_/Console/ConsoleContent.js index 54a9745d0..0b56ead45 100644 --- a/frontend/app/components/Session_/Console/ConsoleContent.js +++ b/frontend/app/components/Session_/Console/ConsoleContent.js @@ -83,11 +83,12 @@ export default class ConsoleContent extends React.PureComponent { @@ -101,7 +102,7 @@ export default class ConsoleContent extends React.PureComponent { {filtered.map((l, index) => (
-
+ {/*
{Duration.fromMillis(l.time).toFormat('mm:ss.SSS')} -
-
+
*/} +
{renderWithNL(l.value)}
diff --git a/frontend/app/components/Session_/Console/console.module.css b/frontend/app/components/Session_/Console/console.module.css index 6f7079d94..2da78f540 100644 --- a/frontend/app/components/Session_/Console/console.module.css +++ b/frontend/app/components/Session_/Console/console.module.css @@ -15,6 +15,9 @@ display: flex; align-items: flex-start; border-bottom: solid thin $gray-light-shade; + &:hover { + background-coor: $active-blue !important; + } } .timestamp { diff --git a/frontend/app/components/Session_/Network/NetworkContent.js b/frontend/app/components/Session_/Network/NetworkContent.js index 082c87aa0..e0f91587c 100644 --- a/frontend/app/components/Session_/Network/NetworkContent.js +++ b/frontend/app/components/Session_/Network/NetworkContent.js @@ -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 ( - {r.url}} - > -
{r.name}
-
+ {r.url}}> +
{r.name}
+
); } export function renderStart(r) { return (
- - {Duration.fromMillis(r.time).toFormat('mm:ss.SSS')} - - -
- ) + */} + + ); } const renderXHRText = () => ( @@ -243,39 +238,45 @@ export default class NetworkContent extends React.PureComponent { iconPosition="left" name="filter" onChange={this.onFilterChange} + height={28} /> - - - 0} - /> - 0} - /> - - - - +
+
+ {}} label="4xx-5xx Only" /> +
+ + + 0} + /> + 0} + /> + + + + +
@@ -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', diff --git a/frontend/app/components/Session_/Performance/Performance.tsx b/frontend/app/components/Session_/Performance/Performance.tsx index 13c135f7b..994401141 100644 --- a/frontend/app/components/Session_/Performance/Performance.tsx +++ b/frontend/app/components/Session_/Performance/Performance.tsx @@ -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 }) => ( - - - + + + ); - -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 ( -
- {"Page is not active. User switched the tab or hid the window."} +
+ {'Page is not active. User switched the tab or hid the window.'}
); } @@ -79,9 +74,9 @@ const FPSTooltip = ({ active, payload }) => { } return ( -
- {`${ FPS }: `} - { Math.trunc(payload[0].value) } +
+ {`${FPS}: `} + {Math.trunc(payload[0].value)}
); }; @@ -91,47 +86,47 @@ const CPUTooltip = ({ active, payload }) => { return null; } return ( -
- {`${ CPU }: `} - { payload[0].value - CPU_VISUAL_OFFSET } - {"%"} +
+ {`${CPU}: `} + {payload[0].value - CPU_VISUAL_OFFSET} + {'%'}
); }; -const HeapTooltip = ({ active, payload}) => { +const HeapTooltip = ({ active, payload }) => { if (!active || payload.length < 2) return null; return ( -
+

- {`${ TOTAL_HEAP }: `} - { formatBytes(payload[0].value)} + {`${TOTAL_HEAP}: `} + {formatBytes(payload[0].value)}

- {`${ USED_HEAP }: `} - { formatBytes(payload[1].value)} + {`${USED_HEAP}: `} + {formatBytes(payload[1].value)}

); -} +}; -const NodesCountTooltip = ({ active, payload} ) => { +const NodesCountTooltip = ({ active, payload }) => { if (!active || !payload || payload.length === 0) return null; return ( -
+

- {`${ NODES_COUNT }: `} - { payload[0].value } + {`${NODES_COUNT}: `} + {payload[0].value}

); -} +}; const TICKS_COUNT = 10; function generateTicks(data: Array): Array { 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 ( -
- Performance +
+
Performance
{/* */} = 1000 - ? `${ connBandwidth / 1000 } Mbps` - : `${ connBandwidth } Kbps` + value={ + connBandwidth >= 1000 ? `${connBandwidth / 1000} Mbps` : `${connBandwidth} Kbps` } - display={ connBandwidth != null } + display={connBandwidth != null} />
- { fps && - + {fps && ( + - + {/* */} {/* */} - - - + {/* */} - + style: { cursor: 'pointer' }, + }} + isAnimationActive={false} + /> - {/* */} - + - } - { cpu && - + )} + {cpu && ( + - + {/* */} - ""} - domain={[0, 'dataMax']} - ticks={this._timeTicks} - > - - - - - - - - - } - - { heap && - - - - - - {/* */} - ""} // tick={false} + this._timeTicks to cartesian array + tickFormatter={() => ''} domain={[0, 'dataMax']} ticks={this._timeTicks} > - + + + + + + + + )} + + {heap && ( + + + + + + {/* */} + ''} // tick={false} + this._timeTicks to cartesian array + domain={[0, 'dataMax']} + ticks={this._timeTicks} + > + max*1.2]} + domain={[0, (max) => max * 1.2]} /> - - + - } - { nodes && - + )} + {nodes && ( + - + {/* */} - ""} - domain={[0, 'dataMax']} - ticks={this._timeTicks} - > - - } + )} ); } } -export const ConnectedPerformance = connectPlayer(state => ({ +export const ConnectedPerformance = connectPlayer((state) => ({ performanceChartTime: state.performanceChartTime, performanceChartData: state.performanceChartData, connType: state.connType, diff --git a/frontend/app/components/Session_/Performance/performance.module.css b/frontend/app/components/Session_/Performance/performance.module.css index 4bd075eec..5c2b85578 100644 --- a/frontend/app/components/Session_/Performance/performance.module.css +++ b/frontend/app/components/Session_/Performance/performance.module.css @@ -3,4 +3,5 @@ padding: 2px 5px; border-radius: 3px; border: 1px solid #ccc; + color: $gray-dark !important; } \ No newline at end of file diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index babe4f2b0..b0fcf583a 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -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 { } { bottomBlock === NETWORK && - + // + } { bottomBlock === STACKEVENTS && diff --git a/frontend/app/components/Session_/TimeTable/timeTable.module.css b/frontend/app/components/Session_/TimeTable/timeTable.module.css index 17feaf459..c2412ff8d 100644 --- a/frontend/app/components/Session_/TimeTable/timeTable.module.css +++ b/frontend/app/components/Session_/TimeTable/timeTable.module.css @@ -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; */ diff --git a/frontend/app/components/shared/DevTools/BottomBlock/BottomBlock.js b/frontend/app/components/shared/DevTools/BottomBlock/BottomBlock.js new file mode 100644 index 000000000..069757e60 --- /dev/null +++ b/frontend/app/components/shared/DevTools/BottomBlock/BottomBlock.js @@ -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 +}) => ( +
+ { children } +
+); + +BottomBlock.displayName = 'BottomBlock'; + +export default BottomBlock; diff --git a/frontend/app/components/shared/DevTools/BottomBlock/Content.js b/frontend/app/components/shared/DevTools/BottomBlock/Content.js new file mode 100644 index 000000000..3df383911 --- /dev/null +++ b/frontend/app/components/shared/DevTools/BottomBlock/Content.js @@ -0,0 +1,17 @@ +import React from 'react'; +import cn from 'classnames'; +import stl from './content.module.css'; + +const Content = ({ + children, + className, + ...props +}) => ( +
+ { children } +
+); + +Content.displayName = 'Content'; + +export default Content; diff --git a/frontend/app/components/shared/DevTools/BottomBlock/Header.js b/frontend/app/components/shared/DevTools/BottomBlock/Header.js new file mode 100644 index 000000000..15dd7a0c9 --- /dev/null +++ b/frontend/app/components/shared/DevTools/BottomBlock/Header.js @@ -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 +}) => ( +
+
+
{ children }
+ { showClose && } +
+
+); + +Header.displayName = 'Header'; + +export default connect(null, { closeBottomBlock })(Header); diff --git a/frontend/app/components/shared/DevTools/BottomBlock/InfoLine.js b/frontend/app/components/shared/DevTools/BottomBlock/InfoLine.js new file mode 100644 index 000000000..3059c70d3 --- /dev/null +++ b/frontend/app/components/shared/DevTools/BottomBlock/InfoLine.js @@ -0,0 +1,20 @@ +import React from 'react'; +import cn from 'classnames'; +import cls from './infoLine.module.css'; + +const InfoLine = ({ children }) => ( +
+ { children } +
+) + +const Point = ({ label = '', value = '', display=true, color, dotColor }) => display + ?
+ { dotColor != null &&
} + { `${label}` } { value } +
+ : null; + +InfoLine.Point = Point; + +export default InfoLine; diff --git a/frontend/app/components/shared/DevTools/BottomBlock/bottomBlock.module.css b/frontend/app/components/shared/DevTools/BottomBlock/bottomBlock.module.css new file mode 100644 index 000000000..99bdd42b4 --- /dev/null +++ b/frontend/app/components/shared/DevTools/BottomBlock/bottomBlock.module.css @@ -0,0 +1,9 @@ + +.wrapper { + background: $white; + /* padding-right: 10px; */ + /* border: solid thin $gray-light; */ + height: 300px; + + border-top: thin dashed #cccccc; +} diff --git a/frontend/app/components/shared/DevTools/BottomBlock/content.module.css b/frontend/app/components/shared/DevTools/BottomBlock/content.module.css new file mode 100644 index 000000000..fe8303013 --- /dev/null +++ b/frontend/app/components/shared/DevTools/BottomBlock/content.module.css @@ -0,0 +1,3 @@ +.content { + height: 86%; +} \ No newline at end of file diff --git a/frontend/app/components/shared/DevTools/BottomBlock/header.module.css b/frontend/app/components/shared/DevTools/BottomBlock/header.module.css new file mode 100644 index 000000000..99faa61c7 --- /dev/null +++ b/frontend/app/components/shared/DevTools/BottomBlock/header.module.css @@ -0,0 +1,6 @@ + +.header { + padding: 0 10px; + height: 40px; + border-bottom: 1px solid $gray-light; +} \ No newline at end of file diff --git a/frontend/app/components/shared/DevTools/BottomBlock/index.js b/frontend/app/components/shared/DevTools/BottomBlock/index.js new file mode 100644 index 000000000..846d7ec6f --- /dev/null +++ b/frontend/app/components/shared/DevTools/BottomBlock/index.js @@ -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; \ No newline at end of file diff --git a/frontend/app/components/shared/DevTools/BottomBlock/infoLine.module.css b/frontend/app/components/shared/DevTools/BottomBlock/infoLine.module.css new file mode 100644 index 000000000..37e47f013 --- /dev/null +++ b/frontend/app/components/shared/DevTools/BottomBlock/infoLine.module.css @@ -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; +} + diff --git a/frontend/app/components/shared/DevTools/BottomBlock/tabs.js b/frontend/app/components/shared/DevTools/BottomBlock/tabs.js new file mode 100644 index 000000000..6addd161e --- /dev/null +++ b/frontend/app/components/shared/DevTools/BottomBlock/tabs.js @@ -0,0 +1,9 @@ +// import { NONE, CONSOLE, NETWORK, STACKEVENTS, REDUX_STATE, PROFILER, PERFORMANCE, GRAPHQL } from 'Duck/components/player'; +// +// +// export default { +// [NONE]: { +// Component: null, +// +// } +// } \ No newline at end of file diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx new file mode 100644 index 000000000..0b8b3a39c --- /dev/null +++ b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx @@ -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 ( + {r.type}
}> +
{r.type}
+ + ); +} + +export function renderName(r: any) { + return ( + {r.url}
}> +
{r.name}
+ + ); +} + +export function renderStart(r: any) { + return ( +
+ {Duration.fromMillis(r.time).toFormat('mm:ss.SSS')} +
+ ); +} + +const renderXHRText = () => ( + + {XHR} + + Use our{' '} + + Fetch plugin + + {' to capture HTTP requests and responses, including status codes and bodies.'}
+ We also provide{' '} + + support for GraphQL + + {' for easy debugging of your queries.'} + + } + className="ml-1" + /> +
+); + +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 = ( +
    + {showTransferred && ( +
  • {`${formatBytes(r.encodedBodySize + headerSize)} transfered over network`}
  • + )} +
  • {`Resource size: ${formatBytes(r.decodedBodySize)} `}
  • +
+ ); + } + + return ( + +
{triggerText}
+
+ ); +} + +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 ( + +
{text}
+
+ ); +} + +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(, { right: true }) + } + + return ( + + + +
+ Network + +
+ +
+ +
+
+ setShowOnlyErrors(!showOnlyErrors)} label="4xx-5xx Only" /> +
+ + + 0} + /> + 0} + /> + + + + +
+ + + No Data +
+ } + size="small" + show={filtered.length === 0} + > + + {[ + // { + // 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, + }, + ]} + + + + + + ); +} + +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); diff --git a/frontend/app/components/shared/DevTools/TimeTable/BarRow.tsx b/frontend/app/components/shared/DevTools/TimeTable/BarRow.tsx new file mode 100644 index 000000000..9de1a8279 --- /dev/null +++ b/frontend/app/components/shared/DevTools/TimeTable/BarRow.tsx @@ -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 = ( +
+
+
+
+ ); + if (!popup) return
{trigger}
; + + return ( +
+ + {ttfb != null && +
+
{'Waiting (TTFB)'}
+
+
+
+
{formatTime(ttfb)}
+
+ } +
+
{'Content Download'}
+
+
+
+
{formatTime(duration - ttfb)}
+
+ + } + size="mini" + position="top center" + /> +
+ ); +} + +BarRow.displayName = "BarRow"; + +export default BarRow; \ No newline at end of file diff --git a/frontend/app/components/shared/DevTools/TimeTable/TimeTable.tsx b/frontend/app/components/shared/DevTools/TimeTable/TimeTable.tsx new file mode 100644 index 000000000..c6e3ea716 --- /dev/null +++ b/frontend/app/components/shared/DevTools/TimeTable/TimeTable.tsx @@ -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; + 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; + children: Array; + 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, 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 { + 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(); + 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 ( +
activeIndex, + })} + onClick={typeof onRowClick === 'function' ? () => onRowClick(row, index) : undefined} + id="table-row" + > + {columns.map(({ dataKey, render, width }) => ( +
+ {render ? render(row) : row[dataKey || ''] || {'empty'}} +
+ ))} +
+ +
+
+ ); + }; + + 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 ( +
+ {navigation && ( +
+
+ )} +
+
+ {columns.map(({ label, width }) => ( +
+ {label} +
+ ))} +
+
+ {timeColumns.map((time, i) => ( +
+ {formatTime(time)} +
+ ))} +
+
+ + +
+
+ {timeColumns.map((_, index) => ( +
+ ))} + {visibleRefLines.map(({ time, color, onClick }) => ( +
+ ))} +
+ + {({ width }: { width: number }) => ( + + )} + +
+ +
+ ); + } +} \ No newline at end of file diff --git a/frontend/app/components/shared/DevTools/TimeTable/barRow.module.css b/frontend/app/components/shared/DevTools/TimeTable/barRow.module.css new file mode 100644 index 000000000..e45d1f7b2 --- /dev/null +++ b/frontend/app/components/shared/DevTools/TimeTable/barRow.module.css @@ -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; +} \ No newline at end of file diff --git a/frontend/app/components/shared/DevTools/TimeTable/index.js b/frontend/app/components/shared/DevTools/TimeTable/index.js new file mode 100644 index 000000000..c3c329b0a --- /dev/null +++ b/frontend/app/components/shared/DevTools/TimeTable/index.js @@ -0,0 +1 @@ +export { default } from './TimeTable'; \ No newline at end of file diff --git a/frontend/app/components/shared/DevTools/TimeTable/timeTable.module.css b/frontend/app/components/shared/DevTools/TimeTable/timeTable.module.css new file mode 100644 index 000000000..c2412ff8d --- /dev/null +++ b/frontend/app/components/shared/DevTools/TimeTable/timeTable.module.css @@ -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; +} \ No newline at end of file diff --git a/frontend/app/components/shared/DevTools/autoscroll.module.css b/frontend/app/components/shared/DevTools/autoscroll.module.css new file mode 100644 index 000000000..209badfb2 --- /dev/null +++ b/frontend/app/components/shared/DevTools/autoscroll.module.css @@ -0,0 +1,12 @@ +.navButtons { + position: absolute; + + background: rgba(255, 255, 255, 0.5); + padding: 4px; + + right: 24px; + top: 8px; + z-index: 1; +} + + diff --git a/frontend/app/components/shared/FetchDetailsModal/FetchDetailsModal.js b/frontend/app/components/shared/FetchDetailsModal/FetchDetailsModal.js index 6f3ad549e..ef29ff128 100644 --- a/frontend/app/components/shared/FetchDetailsModal/FetchDetailsModal.js +++ b/frontend/app/components/shared/FetchDetailsModal/FetchDetailsModal.js @@ -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 (
-
{'URL'}
-
{url}
+
Network Request
+
+
Name
+
+ {resource.name} +
+
+ +
+
Type
+
+ {resource.type} +
+
+ + {method && ( +
+
Request Method
+
+ {resource.method} +
+
+ )} + + {resource.status && ( +
+
Status
+
+ {resource.status === '200' &&
} + {resource.status} +
+
+ )} + + {!!_duration && ( +
+
Time
+
+ {_duration} ms +
+
+ )} + + {/*
{url}
*/}
- {method && ( + {/* {method && (
Method
{method}
@@ -156,7 +192,7 @@ export default class FetchDetailsModal extends React.PureComponent {
Duration
{_duration } ms
- )} + )} */}
diff --git a/frontend/app/components/ui/Input/Input.tsx b/frontend/app/components/ui/Input/Input.tsx index 1c36f7a8a..b90a499f3 100644 --- a/frontend/app/components/ui/Input/Input.tsx +++ b/frontend/app/components/ui/Input/Input.tsx @@ -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 (
{icon && } @@ -29,7 +30,7 @@ const Input = React.forwardRef((props: Props, ref: any) => {