857 lines
24 KiB
TypeScript
857 lines
24 KiB
TypeScript
import { ResourceType, Timed } from 'Player';
|
|
import { WsChannel } from 'Player/web/messages';
|
|
import MobilePlayer from 'Player/mobile/IOSPlayer';
|
|
import WebPlayer from 'Player/web/WebPlayer';
|
|
import { observer } from 'mobx-react-lite';
|
|
import React, {
|
|
useMemo,
|
|
useState,
|
|
useEffect,
|
|
useCallback,
|
|
useRef,
|
|
} from 'react';
|
|
import i18n from 'App/i18n'
|
|
|
|
import { useModal } from 'App/components/Modal';
|
|
import {
|
|
MobilePlayerContext,
|
|
PlayerContext,
|
|
} from 'App/components/Session/playerContext';
|
|
import { formatMs } from 'App/date';
|
|
import { useStore } from 'App/mstore';
|
|
import { formatBytes, debounceCall } from 'App/utils';
|
|
import { Icon, NoContent, Tabs } from 'UI';
|
|
import { Tooltip, Input, Switch, Form } from 'antd';
|
|
import {
|
|
SearchOutlined,
|
|
InfoCircleOutlined,
|
|
} from '@ant-design/icons';
|
|
|
|
import FetchDetailsModal from 'Shared/FetchDetailsModal';
|
|
|
|
import BottomBlock from '../BottomBlock';
|
|
import InfoLine from '../BottomBlock/InfoLine';
|
|
import TabSelector from '../TabSelector';
|
|
import TimeTable from '../TimeTable';
|
|
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
|
import WSPanel from './WSPanel';
|
|
import { mergeListsWithZoom, processInChunks } from './utils'
|
|
|
|
// Constants remain the same
|
|
const INDEX_KEY = 'network';
|
|
const ALL = 'ALL';
|
|
const XHR = 'xhr';
|
|
const JS = 'js';
|
|
const CSS = 'css';
|
|
const IMG = 'img';
|
|
const MEDIA = 'media';
|
|
const OTHER = 'other';
|
|
const WS = 'websocket';
|
|
|
|
const TYPE_TO_TAB = {
|
|
[ResourceType.XHR]: XHR,
|
|
[ResourceType.FETCH]: XHR,
|
|
[ResourceType.SCRIPT]: JS,
|
|
[ResourceType.CSS]: CSS,
|
|
[ResourceType.IMG]: IMG,
|
|
[ResourceType.MEDIA]: MEDIA,
|
|
[ResourceType.WS]: WS,
|
|
[ResourceType.OTHER]: OTHER,
|
|
};
|
|
|
|
const TAP_KEYS = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER, WS] as const;
|
|
export const NETWORK_TABS = TAP_KEYS.map((tab) => ({
|
|
text: tab === 'xhr' ? 'Fetch/XHR' : tab,
|
|
key: tab,
|
|
}));
|
|
|
|
const DOM_LOADED_TIME_COLOR = 'teal';
|
|
const LOAD_TIME_COLOR = 'red';
|
|
|
|
const BATCH_SIZE = 2500;
|
|
const INITIAL_LOAD_SIZE = 5000;
|
|
|
|
export function renderType(r: any) {
|
|
return (
|
|
<Tooltip style={{ width: '100%' }} title={<div>{r.type}</div>}>
|
|
<div>{r.type}</div>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
export function renderName(r: any) {
|
|
return (
|
|
<Tooltip style={{ width: '100%' }} title={<div>{r.url}</div>}>
|
|
<div>{r.name}</div>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
function renderSize(r: any) {
|
|
if (r.responseBodySize) return formatBytes(r.responseBodySize);
|
|
const notCaptured = 'Not captured';
|
|
const resSizeStr = 'Resource size'
|
|
let triggerText;
|
|
let content;
|
|
if (r.responseBodySize) {
|
|
triggerText = formatBytes(r.responseBodySize);
|
|
content = undefined;
|
|
} else if (r.decodedBodySize == null || r.decodedBodySize === 0) {
|
|
triggerText = 'x';
|
|
content = notCaptured;
|
|
} else {
|
|
const headerSize = r.headerSize || 0;
|
|
const showTransferred = r.headerSize != null;
|
|
|
|
triggerText = formatBytes(r.decodedBodySize);
|
|
content = (
|
|
<ul>
|
|
{showTransferred && (
|
|
<li>{`${formatBytes(
|
|
r.encodedBodySize + headerSize
|
|
)} transferred over network`}</li>
|
|
)}
|
|
<li>{`${resSizeStr}: ${formatBytes(r.decodedBodySize)} `}</li>
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Tooltip style={{ width: '100%' }} title={content}>
|
|
<div>{triggerText}</div>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
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;
|
|
if (r.isYellow) {
|
|
tooltipText = 'Slower than average';
|
|
} else {
|
|
tooltipText = 'Much slower than average';
|
|
}
|
|
|
|
return (
|
|
<Tooltip style={{ width: '100%' }} title={tooltipText}>
|
|
<div> {text} </div>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
function renderStatus({
|
|
status,
|
|
cached,
|
|
error,
|
|
}: {
|
|
status: string;
|
|
cached: boolean;
|
|
error?: string;
|
|
}) {
|
|
const displayedStatus = error ? (
|
|
<Tooltip title={error}>
|
|
<div
|
|
style={{ width: 90 }}
|
|
className={'overflow-hidden overflow-ellipsis'}
|
|
>
|
|
{error}
|
|
</div>
|
|
</Tooltip>
|
|
) : (
|
|
status
|
|
);
|
|
return (
|
|
<>
|
|
{cached ? (
|
|
<Tooltip title={'Served from cache'} placement="top">
|
|
<div className="flex items-center">
|
|
<span className="mr-1">{displayedStatus}</span>
|
|
<Icon name="wifi" size={16} />
|
|
</div>
|
|
</Tooltip>
|
|
) : (
|
|
displayedStatus
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
|
|
// Main component for Network Panel
|
|
function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
|
const { player, store } = React.useContext(PlayerContext);
|
|
const { sessionStore, uiPlayerStore } = useStore();
|
|
|
|
const startedAt = sessionStore.current.startedAt;
|
|
const {
|
|
domContentLoadedTime,
|
|
loadTime,
|
|
domBuildingTime,
|
|
tabStates,
|
|
currentTab,
|
|
tabNames,
|
|
} = store.get();
|
|
const tabsArr = Object.keys(tabStates);
|
|
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]);
|
|
const getTabNum = (tab: string) => tabsArr.findIndex((t) => t === tab) + 1;
|
|
const getTabName = (tabId: string) => tabNames[tabId]
|
|
return (
|
|
<NetworkPanelComp
|
|
loadTime={loadTime}
|
|
panelHeight={panelHeight}
|
|
domBuildingTime={domBuildingTime}
|
|
domContentLoadedTime={domContentLoadedTime}
|
|
fetchList={fetchList}
|
|
resourceList={resourceList}
|
|
fetchListNow={fetchListNow}
|
|
resourceListNow={resourceListNow}
|
|
player={player}
|
|
startedAt={startedAt}
|
|
websocketList={websocketList}
|
|
websocketListNow={websocketListNow}
|
|
getTabNum={getTabNum}
|
|
getTabName={getTabName}
|
|
showSingleTab={showSingleTab}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function MobileNetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
|
const { player, store } = React.useContext(MobilePlayerContext);
|
|
const { uiPlayerStore, sessionStore } = useStore();
|
|
const startedAt = sessionStore.current.startedAt;
|
|
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
|
|
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
|
|
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
|
|
const domContentLoadedTime = undefined;
|
|
const loadTime = undefined;
|
|
const domBuildingTime = undefined;
|
|
const {
|
|
fetchList = [],
|
|
resourceList = [],
|
|
fetchListNow = [],
|
|
resourceListNow = [],
|
|
websocketList = [],
|
|
websocketListNow = [],
|
|
} = store.get();
|
|
|
|
return (
|
|
<NetworkPanelComp
|
|
isMobile
|
|
panelHeight={panelHeight}
|
|
loadTime={loadTime}
|
|
domBuildingTime={domBuildingTime}
|
|
domContentLoadedTime={domContentLoadedTime}
|
|
fetchList={fetchList}
|
|
resourceList={resourceList}
|
|
fetchListNow={fetchListNow}
|
|
resourceListNow={resourceListNow}
|
|
player={player}
|
|
startedAt={startedAt}
|
|
websocketList={websocketList}
|
|
websocketListNow={websocketListNow}
|
|
zoomEnabled={zoomEnabled}
|
|
zoomStartTs={zoomStartTs}
|
|
zoomEndTs={zoomEndTs}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const useInfiniteScroll = (loadMoreCallback: () => void, hasMore: boolean) => {
|
|
const observerRef = useRef<IntersectionObserver>(null);
|
|
const loadingRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
if (entries[0]?.isIntersecting && hasMore) {
|
|
loadMoreCallback();
|
|
}
|
|
},
|
|
{ threshold: 0.1 },
|
|
);
|
|
|
|
if (loadingRef.current) {
|
|
observer.observe(loadingRef.current);
|
|
}
|
|
|
|
// @ts-ignore
|
|
observerRef.current = observer;
|
|
|
|
return () => {
|
|
if (observerRef.current) {
|
|
observerRef.current.disconnect();
|
|
}
|
|
};
|
|
}, [loadMoreCallback, hasMore, loadingRef]);
|
|
|
|
return loadingRef;
|
|
};
|
|
|
|
interface Props {
|
|
domContentLoadedTime?: {
|
|
time: number;
|
|
value: number;
|
|
};
|
|
loadTime?: {
|
|
time: number;
|
|
value: number;
|
|
};
|
|
domBuildingTime?: number;
|
|
fetchList: Timed[];
|
|
resourceList: Timed[];
|
|
fetchListNow: Timed[];
|
|
resourceListNow: Timed[];
|
|
websocketList: Array<WsChannel>;
|
|
websocketListNow: Array<WsChannel>;
|
|
player: WebPlayer | MobilePlayer;
|
|
startedAt: number;
|
|
isMobile?: boolean;
|
|
zoomEnabled?: boolean;
|
|
zoomStartTs?: number;
|
|
zoomEndTs?: number;
|
|
panelHeight: number;
|
|
onClose?: () => void;
|
|
activeOutsideIndex?: number;
|
|
isSpot?: boolean;
|
|
getTabNum?: (tab: string) => number;
|
|
getTabName?: (tabId: string) => string;
|
|
showSingleTab?: boolean;
|
|
}
|
|
|
|
export const NetworkPanelComp = observer(
|
|
({
|
|
loadTime,
|
|
domBuildingTime,
|
|
domContentLoadedTime,
|
|
fetchList,
|
|
resourceList,
|
|
fetchListNow,
|
|
resourceListNow,
|
|
player,
|
|
startedAt,
|
|
isMobile,
|
|
panelHeight,
|
|
websocketList,
|
|
zoomEnabled,
|
|
zoomStartTs,
|
|
zoomEndTs,
|
|
onClose,
|
|
activeOutsideIndex,
|
|
isSpot,
|
|
getTabNum,
|
|
showSingleTab,
|
|
getTabName,
|
|
}: Props) => {
|
|
const [selectedWsChannel, setSelectedWsChannel] = React.useState<
|
|
WsChannel[] | null
|
|
>(null);
|
|
const { showModal } = useModal();
|
|
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
|
|
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
const [displayedItems, setDisplayedItems] = useState([]);
|
|
const [totalItems, setTotalItems] = useState(0);
|
|
const [summaryStats, setSummaryStats] = useState({
|
|
resourcesSize: 0,
|
|
transferredSize: 0,
|
|
});
|
|
|
|
const originalListRef = useRef([]);
|
|
const socketListRef = useRef([]);
|
|
|
|
const {
|
|
sessionStore: { devTools },
|
|
} = useStore();
|
|
const filter = devTools[INDEX_KEY].filter;
|
|
const activeTab = devTools[INDEX_KEY].activeTab;
|
|
const activeIndex = activeOutsideIndex ?? devTools[INDEX_KEY].index;
|
|
const [inputFilterValue, setInputFilterValue] = useState(filter);
|
|
|
|
const debouncedFilter = useCallback(
|
|
debounceCall((filterValue) => {
|
|
devTools.update(INDEX_KEY, { filter: filterValue });
|
|
}, 300),
|
|
[],
|
|
);
|
|
|
|
// Process socket lists once
|
|
useEffect(() => {
|
|
const uniqueSocketList = websocketList.filter(
|
|
(ws, i, arr) =>
|
|
arr.findIndex((it) => it.channelName === ws.channelName) === i,
|
|
);
|
|
socketListRef.current = uniqueSocketList;
|
|
}, [websocketList.length]);
|
|
|
|
// Initial data processing - do this only once when data changes
|
|
useEffect(() => {
|
|
setIsLoading(true);
|
|
|
|
// Heaviest operation here, will create a final merged network list
|
|
const processData = async () => {
|
|
const fetchUrls = new Set(
|
|
fetchList.map((ft) => {
|
|
return `${ft.name}-${Math.floor(ft.time / 100)}-${Math.floor(ft.duration / 100)}`;
|
|
}),
|
|
);
|
|
|
|
// We want to get resources that aren't in fetch list
|
|
const filteredResources = await processInChunks(resourceList, (chunk) =>
|
|
chunk.filter((res: any) => {
|
|
const key = `${res.name}-${Math.floor(res.time / 100)}-${Math.floor(res.duration / 100)}`;
|
|
return !fetchUrls.has(key);
|
|
}),
|
|
BATCH_SIZE,
|
|
25,
|
|
);
|
|
|
|
const processedSockets = socketListRef.current.map((ws: any) => ({
|
|
...ws,
|
|
type: 'websocket',
|
|
method: 'ws',
|
|
url: ws.channelName,
|
|
name: ws.channelName,
|
|
status: '101',
|
|
duration: 0,
|
|
transferredBodySize: 0,
|
|
}));
|
|
|
|
const mergedList: Timed[] = mergeListsWithZoom(
|
|
filteredResources as Timed[],
|
|
fetchList,
|
|
processedSockets as Timed[],
|
|
{ enabled: Boolean(zoomEnabled), start: zoomStartTs ?? 0, end: zoomEndTs ?? 0 }
|
|
)
|
|
|
|
originalListRef.current = mergedList;
|
|
setTotalItems(mergedList.length);
|
|
|
|
calculateResourceStats(resourceList);
|
|
|
|
// Only display initial chunk
|
|
setDisplayedItems(mergedList.slice(0, INITIAL_LOAD_SIZE));
|
|
setIsLoading(false);
|
|
};
|
|
|
|
void processData();
|
|
}, [
|
|
resourceList.length,
|
|
fetchList.length,
|
|
socketListRef.current.length,
|
|
zoomEnabled,
|
|
zoomStartTs,
|
|
zoomEndTs,
|
|
]);
|
|
|
|
const calculateResourceStats = (resourceList: Record<string, any>) => {
|
|
setTimeout(() => {
|
|
let resourcesSize = 0
|
|
let transferredSize = 0
|
|
resourceList.forEach(({ decodedBodySize, headerSize, encodedBodySize }: any) => {
|
|
resourcesSize += decodedBodySize || 0
|
|
transferredSize += (headerSize || 0) + (encodedBodySize || 0)
|
|
})
|
|
|
|
setSummaryStats({
|
|
resourcesSize,
|
|
transferredSize,
|
|
});
|
|
}, 0);
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (originalListRef.current.length === 0) return;
|
|
setIsProcessing(true);
|
|
const applyFilters = async () => {
|
|
let filteredItems: any[] = originalListRef.current;
|
|
|
|
filteredItems = await processInChunks(filteredItems, (chunk) =>
|
|
chunk.filter(
|
|
(it) => {
|
|
let valid = true;
|
|
if (showOnlyErrors) {
|
|
valid = parseInt(it.status) >= 400 || !it.success || it.error
|
|
}
|
|
if (filter) {
|
|
try {
|
|
const regex = new RegExp(filter, 'i');
|
|
valid = valid && regex.test(it.status) || regex.test(it.name) || regex.test(it.type) || regex.test(it.method);
|
|
} catch (e) {
|
|
valid = valid && String(it.status).includes(filter) || it.name.includes(filter) || it.type.includes(filter) || (it.method && it.method.includes(filter));
|
|
}
|
|
}
|
|
if (activeTab !== ALL) {
|
|
valid = valid && TYPE_TO_TAB[it.type] === activeTab;
|
|
}
|
|
|
|
return valid;
|
|
},
|
|
),
|
|
);
|
|
|
|
// Update displayed items
|
|
setDisplayedItems(filteredItems.slice(0, INITIAL_LOAD_SIZE));
|
|
setTotalItems(filteredItems.length);
|
|
setIsProcessing(false);
|
|
};
|
|
|
|
void applyFilters();
|
|
}, [filter, activeTab, showOnlyErrors]);
|
|
|
|
const loadMoreItems = useCallback(() => {
|
|
if (isProcessing) return;
|
|
|
|
setIsProcessing(true);
|
|
setTimeout(() => {
|
|
setDisplayedItems((prevItems) => {
|
|
const currentLength = prevItems.length;
|
|
const newItems = originalListRef.current.slice(
|
|
currentLength,
|
|
currentLength + BATCH_SIZE,
|
|
);
|
|
return [...prevItems, ...newItems];
|
|
});
|
|
setIsProcessing(false);
|
|
}, 10);
|
|
}, [isProcessing]);
|
|
|
|
const hasMoreItems = displayedItems.length < totalItems;
|
|
const loadingRef = useInfiniteScroll(loadMoreItems, hasMoreItems);
|
|
|
|
const onTabClick = (activeTab) => {
|
|
devTools.update(INDEX_KEY, { activeTab });
|
|
};
|
|
|
|
const onFilterChange = ({ target: { value } }) => {
|
|
setInputFilterValue(value)
|
|
debouncedFilter(value);
|
|
};
|
|
|
|
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
|
|
displayedItems,
|
|
getLastItemTime(fetchListNow, resourceListNow),
|
|
activeIndex,
|
|
(index) => devTools.update(INDEX_KEY, { index })
|
|
);
|
|
const onMouseEnter = stopAutoscroll;
|
|
const onMouseLeave = () => {
|
|
if (isDetailsModalActive) {
|
|
return;
|
|
}
|
|
timeoutStartAutoscroll();
|
|
};
|
|
|
|
const referenceLines = useMemo(() => {
|
|
const arr = [];
|
|
|
|
if (domContentLoadedTime != null) {
|
|
arr.push({
|
|
time: domContentLoadedTime.time,
|
|
color: DOM_LOADED_TIME_COLOR,
|
|
});
|
|
}
|
|
if (loadTime != null) {
|
|
arr.push({
|
|
time: loadTime.time,
|
|
color: LOAD_TIME_COLOR,
|
|
});
|
|
}
|
|
|
|
return arr;
|
|
}, [domContentLoadedTime, loadTime]);
|
|
|
|
const showDetailsModal = (item: any) => {
|
|
if (item.type === 'websocket') {
|
|
const socketMsgList = websocketList.filter(
|
|
(ws) => ws.channelName === item.channelName
|
|
);
|
|
|
|
return setSelectedWsChannel(socketMsgList);
|
|
}
|
|
setIsDetailsModalActive(true);
|
|
showModal(
|
|
<FetchDetailsModal
|
|
isSpot={isSpot}
|
|
time={item.time + startedAt}
|
|
resource={item}
|
|
rows={displayedItems}
|
|
fetchPresented={fetchList.length > 0}
|
|
/>,
|
|
{
|
|
right: true,
|
|
width: 500,
|
|
onClose: () => {
|
|
setIsDetailsModalActive(false);
|
|
timeoutStartAutoscroll();
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
const tableCols = useMemo(() => {
|
|
const cols = [
|
|
{
|
|
label: 'Status',
|
|
dataKey: 'status',
|
|
width: 90,
|
|
render: renderStatus,
|
|
},
|
|
{
|
|
label: 'Type',
|
|
dataKey: 'type',
|
|
width: 90,
|
|
render: renderType,
|
|
},
|
|
{
|
|
label: 'Method',
|
|
width: 80,
|
|
dataKey: 'method',
|
|
},
|
|
{
|
|
label: 'Name',
|
|
width: 240,
|
|
dataKey: 'name',
|
|
render: renderName,
|
|
},
|
|
{
|
|
label: 'Size',
|
|
width: 80,
|
|
dataKey: 'decodedBodySize',
|
|
render: renderSize,
|
|
hidden: activeTab === XHR,
|
|
},
|
|
{
|
|
label: 'Duration',
|
|
width: 80,
|
|
dataKey: 'duration',
|
|
render: renderDuration,
|
|
},
|
|
];
|
|
if (!showSingleTab && !isSpot) {
|
|
cols.unshift({
|
|
label: 'Source',
|
|
width: 64,
|
|
render: (r: Record<string, any>) => (
|
|
<Tooltip title={`${getTabName?.(r.tabId) ?? `Tab ${getTabNum?.(r.tabId) ?? 0}`}`} placement="left">
|
|
<div className="bg-gray-light rounded-full min-w-5 min-h-5 w-5 h-5 flex items-center justify-center text-xs cursor-default">
|
|
{getTabNum?.(r.tabId) ?? 0}
|
|
</div>
|
|
</Tooltip>
|
|
),
|
|
});
|
|
}
|
|
return cols;
|
|
}, [showSingleTab, activeTab, getTabName, getTabNum, isSpot]);
|
|
|
|
return (
|
|
<BottomBlock
|
|
style={{ height: '100%' }}
|
|
className="border"
|
|
onMouseEnter={onMouseEnter}
|
|
onMouseLeave={onMouseLeave}
|
|
>
|
|
<BottomBlock.Header onClose={onClose}>
|
|
<div className="flex items-center">
|
|
<span className="font-semibold color-gray-medium mr-4">
|
|
Network
|
|
</span>
|
|
{isMobile ? null : (
|
|
<Tabs
|
|
className="uppercase"
|
|
tabs={NETWORK_TABS}
|
|
active={activeTab}
|
|
onClick={onTabClick}
|
|
border={false}
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className={'flex items-center gap-2'}>
|
|
{!isMobile && !isSpot ? <TabSelector /> : null}
|
|
<Input
|
|
className="rounded-lg"
|
|
placeholder="Filter by name, type, method or value"
|
|
name="filter"
|
|
onChange={onFilterChange}
|
|
width={280}
|
|
value={inputFilterValue}
|
|
size="small"
|
|
prefix={<SearchOutlined className="text-neutral-400" />}
|
|
/>
|
|
</div>
|
|
</BottomBlock.Header>
|
|
<BottomBlock.Content>
|
|
<div className="flex items-center justify-between px-4 border-b bg-teal/5 h-8">
|
|
<div className="flex items-center">
|
|
<Form.Item name="show-errors-only" className="mb-0">
|
|
<label
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
<Switch
|
|
checked={showOnlyErrors}
|
|
onChange={() => setShowOnlyErrors(!showOnlyErrors)}
|
|
size="small"
|
|
/>
|
|
<span className="text-sm ms-2">4xx-5xx Only</span>
|
|
</label>
|
|
</Form.Item>
|
|
|
|
{isProcessing && (
|
|
<span className="text-xs text-gray-500 ml-4">
|
|
Processing data...
|
|
</span>
|
|
)}
|
|
</div>
|
|
<InfoLine>
|
|
<InfoLine.Point label={`${totalItems}`} value="requests" />
|
|
<InfoLine.Point
|
|
label={`${displayedItems.length}/${totalItems}`}
|
|
value="displayed"
|
|
display={displayedItems.length < totalItems}
|
|
/>
|
|
<InfoLine.Point
|
|
label={formatBytes(summaryStats.transferredSize)}
|
|
value="transferred"
|
|
display={summaryStats.transferredSize > 0}
|
|
/>
|
|
<InfoLine.Point
|
|
label={formatBytes(summaryStats.resourcesSize)}
|
|
value="resources"
|
|
display={summaryStats.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>
|
|
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-2"></div>
|
|
<p>Processing initial network data...</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<NoContent
|
|
title={
|
|
<div className="capitalize flex items-center gap-2">
|
|
<InfoCircleOutlined size={18} />
|
|
{'No Data'}
|
|
</div>
|
|
}
|
|
size="small"
|
|
show={displayedItems.length === 0}
|
|
>
|
|
<div>
|
|
<TimeTable
|
|
rows={displayedItems}
|
|
tableHeight={panelHeight - 102 - (hasMoreItems ? 30 : 0)}
|
|
referenceLines={referenceLines}
|
|
renderPopup
|
|
onRowClick={showDetailsModal}
|
|
sortBy="time"
|
|
sortAscending
|
|
onJump={(row) => {
|
|
devTools.update(INDEX_KEY, {
|
|
index: displayedItems.indexOf(row),
|
|
});
|
|
player.jump(row.time);
|
|
}}
|
|
activeIndex={activeIndex}
|
|
>
|
|
{tableCols}
|
|
</TimeTable>
|
|
|
|
{hasMoreItems && (
|
|
<div
|
|
ref={loadingRef}
|
|
className="flex justify-center items-center text-xs text-gray-500"
|
|
>
|
|
<div className="flex items-center">
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-600 mr-2"></div>
|
|
Loading more data ({totalItems - displayedItems.length}{' '}
|
|
remaining)
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{selectedWsChannel ? (
|
|
<WSPanel
|
|
socketMsgList={selectedWsChannel}
|
|
onClose={() => setSelectedWsChannel(null)}
|
|
/>
|
|
) : null}
|
|
</NoContent>
|
|
)}
|
|
</BottomBlock.Content>
|
|
</BottomBlock>
|
|
);
|
|
}
|
|
);
|
|
|
|
const WebNetworkPanel = observer(NetworkPanelCont);
|
|
const MobileNetworkPanel = observer(MobileNetworkPanelCont);
|
|
|
|
export { WebNetworkPanel, MobileNetworkPanel };
|