ui: fix network panel re-render

This commit is contained in:
nick-delirium 2025-01-27 12:08:46 +01:00
parent 0f744ec1a0
commit 778112c751
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
5 changed files with 295 additions and 311 deletions

View file

@ -181,39 +181,37 @@ function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
const tabValues = Object.values(tabStates);
const dataSource = uiPlayerStore.dataSource;
const showSingleTab = dataSource === 'current';
const {
fetchList = [],
resourceList = [],
fetchListNow = [],
resourceListNow = [],
websocketList = [],
websocketListNow = [],
} = React.useMemo(() => {
if (showSingleTab) {
return tabStates[currentTab] ?? {};
} else {
const fetchList = tabValues.flatMap((tab) => tab.fetchList);
const resourceList = tabValues.flatMap((tab) => tab.resourceList);
const fetchListNow = tabValues
.flatMap((tab) => tab.fetchListNow)
.filter(Boolean);
const resourceListNow = tabValues
.flatMap((tab) => tab.resourceListNow)
.filter(Boolean);
const websocketList = tabValues.flatMap((tab) => tab.websocketList);
const websocketListNow = tabValues
.flatMap((tab) => tab.websocketListNow)
.filter(Boolean);
return {
fetchList,
resourceList,
fetchListNow,
resourceListNow,
websocketList,
websocketListNow,
};
}
}, [currentTab, tabStates, dataSource, tabValues]);
let fetchList = [];
let resourceList = [];
let fetchListNow = [];
let resourceListNow = [];
let websocketList = [];
let websocketListNow = [];
if (showSingleTab) {
const state = tabStates[currentTab] ?? {};
fetchList = state.fetchList ?? [];
resourceList = state.resourceList ?? [];
fetchListNow = state.fetchListNow ?? [];
resourceListNow = state.resourceListNow ?? [];
websocketList = state.websocketList ?? [];
websocketListNow = state.websocketListNow ?? [];
} else {
fetchList = tabValues.flatMap((tab) => tab.fetchList);
resourceList = tabValues.flatMap((tab) => tab.resourceList);
fetchListNow = tabValues
.flatMap((tab) => tab.fetchListNow)
.filter(Boolean);
resourceListNow = tabValues
.flatMap((tab) => tab.resourceListNow)
.filter(Boolean);
websocketList = tabValues.flatMap((tab) => tab.websocketList);
websocketListNow = tabValues
.flatMap((tab) => tab.websocketListNow)
.filter(Boolean);
}
const getTabNum = (tab: string) => tabsArr.findIndex((t) => t === tab) + 1;
const getTabName = (tabId: string) => tabNames[tabId]
return (
@ -416,7 +414,7 @@ export const NetworkPanelComp = observer(
: true
)
.sort((a, b) => a.time - b.time),
[resourceList.length, fetchList.length, socketList]
[resourceList.length, fetchList.length, socketList.length]
);
let filteredList = useMemo(() => {
@ -453,7 +451,7 @@ export const NetworkPanelComp = observer(
activeIndex,
(index) => devTools.update(INDEX_KEY, { index })
);
const onMouseEnter = stopAutoscroll;
const onMouseEnter = () => stopAutoscroll;
const onMouseLeave = () => {
if (isDetailsModalActive) {
return;

View file

@ -26,40 +26,7 @@ const BarRow = ({
}: Props) => {
const timeOffset = time - timestart;
ttfb = ttfb || 0;
// TODO fix the tooltip
const 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>
);
const trigger = (
<div
className={styles.barWrapper}
@ -101,4 +68,4 @@ const BarRow = ({
BarRow.displayName = 'BarRow';
export default BarRow;
export default React.memo(BarRow);

View file

@ -8,8 +8,9 @@ import { percentOf } from 'App/utils';
import BarRow from './BarRow';
import stl from './timeTable.module.css';
import autoscrollStl from '../autoscroll.module.css'; //aaa
import autoscrollStl from '../autoscroll.module.css';
import JumpButton from '../JumpButton';
import { observer } from 'mobx-react-lite';
type Timed = {
time: number;
@ -45,11 +46,6 @@ type Column = {
onClick?: void;
} & RenderOrKey;
// type RenderOrKey = { // Disjoint?
// render: Row => React.Node
// } | {
// dataKey: string,
// }
type RenderOrKey =
| {
render?: (row: Row) => React.ReactNode;
@ -79,12 +75,8 @@ type TimeLineInfo = {
timewidth: number;
};
type State = TimeLineInfo & typeof initialState;
//const TABLE_HEIGHT = 195;
let _additionalHeight = 0;
const ROW_HEIGHT = 24;
//const VISIBLE_COUNT = Math.ceil(TABLE_HEIGHT/ROW_HEIGHT);
const TIME_SECTIONS_COUNT = 8;
const ZERO_TIMEWIDTH = 1000;
@ -103,10 +95,13 @@ function computeTimeLine(
firstVisibleRowIndex,
firstVisibleRowIndex + visibleCount + _additionalHeight
);
let timestart = visibleRows.length > 0 ? Math.min(...visibleRows.map((r) => r.time)) : 0;
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;
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) {
@ -122,245 +117,267 @@ function computeTimeLine(
};
}
const initialState = {
firstVisibleRowIndex: 0,
};
function TimeTable(props: Props) {
const tableHeight = props.tableHeight || 195;
const visibleCount = Math.ceil(tableHeight / ROW_HEIGHT);
const [timerange, setTimerange] = React.useState({
timestart: 0,
timewidth: 0,
});
const [firstVisibleRowIndex, setFirstVisibleRowIndex] = React.useState(0);
const scroller = React.createRef<VListHandle>();
const { timestart, timewidth } = timerange;
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<VListHandle>();
autoScroll = true;
adjustScroll(prevActiveIndex: number) {
if (
this.props.activeIndex &&
this.props.activeIndex >= 0 &&
prevActiveIndex !== this.props.activeIndex &&
this.scroller.current
) {
this.scroller.current.scrollToIndex(this.props.activeIndex);
React.useEffect(() => {
const { timestart, timewidth } = computeTimeLine(
props.rows,
firstVisibleRowIndex,
visibleCount
);
setTimerange({ timestart, timewidth });
}, [
props.rows.length,
visibleCount,
_additionalHeight,
firstVisibleRowIndex,
]);
React.useEffect(() => {
if (props.activeIndex && props.activeIndex >= 0 && scroller.current) {
scroller.current.scrollToIndex(props.activeIndex);
setFirstVisibleRowIndex(props.activeIndex ?? 0);
}
}
}, [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),
});
}
// this.adjustScroll(prevProps.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 });
const onJump = (index: any) => {
if (props.onJump) {
props.onJump(props.rows[index]);
}
};
onJump = (index: any) => {
if (this.props.onJump) {
this.props.onJump(this.props.rows[index]);
const onPrevClick = () => {
let prevRedIndex = -1;
for (let i = firstVisibleRowIndex - 1; i >= 0; i--) {
if (props.rows[i].isRed) {
prevRedIndex = i;
break;
}
}
if (scroller.current != null) {
scroller.current.scrollToIndex(prevRedIndex);
}
};
renderRow = (index: number) => {
const { activeIndex } = this.props;
const { children: columns, rows, renderPopup, hoverable, onRowClick } = this.props;
const { timestart, timewidth } = this.state;
const row = rows[index];
return (
<div
className={cn(
'dev-row border-b border-neutral-950/5 group items-center text-sm',
stl.row,
{
[stl.hoverable]: hoverable,
'error color-red': 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
.filter((i: any) => !i.hidden)
.map(({ dataKey, render, width, label }) => (
<div key={parseInt(label.replace(' ', '')+dataKey, 36)} className={cn(stl.cell, 'overflow-ellipsis overflow-hidden !py-0.5')} style={{ width: `${width}px` }}>
{render
? render(row)
: row[dataKey || ''] || <i className="color-gray-light">{'empty'}</i>}
const onNextClick = () => {
let prevRedIndex = -1;
for (let i = firstVisibleRowIndex + 1; i < props.rows.length; i++) {
if (props.rows[i].isRed) {
prevRedIndex = i;
break;
}
}
if (scroller.current != null) {
scroller.current.scrollToIndex(prevRedIndex);
}
};
const {
className,
rows,
navigation = false,
referenceLines = [],
additionalHeight = 0,
renderPopup,
hoverable,
onRowClick,
activeIndex,
} = props;
const columns = props.children.filter((i: any) => !i.hidden);
_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={onPrevClick}
/>
<Button
variant="text-primary"
icon="chevron-down"
tooltip={{
title: 'Next Error',
delay: 0,
}}
onClick={onNextClick}
/>
</div>
)}
<div className={stl.headers}>
<div className={stl.infoHeaders}>
{columns.map(({ label, width, dataKey, onClick = null }) => (
<div
key={parseInt(label.replace(' ', ''), 36)}
className={cn(stl.headerCell, 'flex items-center select-none', {
'cursor-pointer': typeof onClick === 'function',
})}
style={{ width: `${width}px` }}
// onClick={() => onColumnClick(dataKey, onClick)}
>
<span>{label}</span>
</div>
))}
<div className={cn('relative flex-1 flex', stl.timeBarWrapper)} style={{ height: 15 }}>
<BarRow resource={row} timestart={timestart} timewidth={timewidth} popup={renderPopup} />
</div>
<JumpButton onClick={() => this.onJump(index)} />
</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.scrollToIndex(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.scrollToIndex(prevRedIndex);
}
};
onColumnClick = (dataKey: string, onClick: any) => {
if (typeof onClick === 'function') {
onClick(dataKey);
}
};
render() {
const {
className,
rows,
navigation = false,
referenceLines = [],
additionalHeight = 0,
activeIndex,
} = this.props;
const columns = this.props.children.filter((i: any) => !i.hidden);
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, dataKey, onClick = null }) => (
<div
key={parseInt(label.replace(' ', ''), 36)}
className={cn(stl.headerCell, 'flex items-center select-none', {
'cursor-pointer': typeof onClick === 'function',
})}
style={{ width: `${width}px` }}
// onClick={() => this.onColumnClick(dataKey, onClick)}
>
<span>{label}</span>
</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" style={{ height: this.tableHeight }}>
<div className={stl.timePart} style={{ left: `${columnsSumWidth}px` }}>
{timeColumns.map((_, index) => (
<div key={`tc-${index}`} className={stl.timeCell} />
))}
{visibleRefLines.map(({ time, color, onClick }) => (
<div
key={time}
className={cn(stl.refLine, `bg-${color}`)}
style={{
left: `${percentOf(time - timestart, timewidth)}%`,
cursor: typeof onClick === 'function' ? 'click' : 'auto',
}}
onClick={onClick}
/>
))}
<div className={stl.waterfallHeaders}>
{timeColumns.map((time, i) => (
<div className={stl.timeCell} key={`tc-${i}`}>
{formatTime(time)}
</div>
<VList className={stl.list} ref={this.scroller} itemSize={ROW_HEIGHT} count={rows.length}>
{this.props.rows.map((_, index) => this.renderRow(index))}
</VList>
</div>
</NoContent>
))}
</div>
</div>
);
}
<NoContent size="small" show={rows.length === 0}>
<div className="relative" style={{ height: tableHeight }}>
<div
className={stl.timePart}
style={{ left: `${columnsSumWidth}px` }}
>
{timeColumns.map((_, index) => (
<div key={`tc-${index}`} className={stl.timeCell} />
))}
{visibleRefLines.map(({ time, color, onClick }) => (
<div
key={time}
className={cn(stl.refLine, `bg-${color}`)}
style={{
left: `${percentOf(time - timestart, timewidth)}%`,
cursor: typeof onClick === 'function' ? 'click' : 'auto',
}}
onClick={onClick}
/>
))}
</div>
<VList
className={stl.list}
ref={scroller}
itemSize={ROW_HEIGHT}
count={rows.length}
overscan={10}
onScroll={(offset) => {
const firstVisibleRowIndex = Math.floor(
offset / ROW_HEIGHT + 0.33
);
setFirstVisibleRowIndex(firstVisibleRowIndex);
}}
>
{(index) => (
<RowRenderer
row={rows[index]}
index={index}
columns={columns}
timestart={timestart}
timewidth={timewidth}
renderPopup={renderPopup}
hoverable={hoverable}
onRowClick={onRowClick}
activeIndex={activeIndex}
onJump={onJump}
/>
)}
</VList>
</div>
</NoContent>
</div>
);
}
function RowRenderer({
index,
row,
columns,
timestart,
timewidth,
renderPopup,
hoverable,
onRowClick,
activeIndex,
onJump,
}: any) {
return (
<div
className={cn(
'dev-row border-b border-neutral-950/5 group items-center text-sm',
stl.row,
{
[stl.hoverable]: hoverable,
'error color-red': 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"
>
<RowColumns columns={columns} row={row} />
<div
className={cn('relative flex-1 flex', stl.timeBarWrapper)}
style={{ height: 15 }}
>
<BarRow
resource={row}
timestart={timestart}
timewidth={timewidth}
popup={renderPopup}
/>
</div>
<JumpButton onClick={() => onJump(index)} />
</div>
);
}
const RowColumns = React.memo(({ columns, row }: any) => {
return columns.map(({ dataKey, render, width, label }: any) => (
<div
key={label.replace(' ', '') + dataKey}
className={cn(
stl.cell,
'overflow-ellipsis overflow-hidden !py-0.5'
)}
style={{ width: `${width}px` }}
>
{render
? render(row)
: row[dataKey || ''] || (
<i className="color-gray-light">{'empty'}</i>
)}
</div>
))
})
export default observer(TimeTable);

View file

@ -16,9 +16,9 @@ function useAutoupdate<T>(
updadteValue: (value: T) => void,
) {
const [ autoupdate, setAutoupdate ] = useState(savedValue === resetValue)
const [ timeoutStartAutoupdate, stopAutoupdate ] = useCancelableTimeout(
() => setAutoupdate(true),
() => setAutoupdate(true),
() => setAutoupdate(false),
TIMEOUT_DURATION,
)
@ -61,4 +61,4 @@ export default function useAutoscroll(
}, [ time, filteredList ])
return useAutoupdate(savedIndex, filteredIndexNow, 0, updadteIndex)
}
}

View file

@ -131,6 +131,7 @@ export function getResourceFromResourceTiming(msg: ResourceTiming, sessStart: nu
// duration might be duration=0 when cached
const failed = msg.duration === 0 && msg.ttfb === 0 && msg.headerSize === 0 && msg.encodedBodySize === 0 && msg.transferredSize === 0
const type = getResourceType(msg.initiator, msg.url)
console.log(msg.url, msg.timestamp - sessStart)
return Resource({
...msg,
type,
@ -142,6 +143,7 @@ export function getResourceFromResourceTiming(msg: ResourceTiming, sessStart: nu
}
export function getResourceFromNetworkRequest(msg: NetworkRequest | Fetch | MobileNetworkCall, sessStart: number) {
console.log(msg.url, msg.timestamp - sessStart)
return Resource({
...msg,
// @ts-ignore