From 0e3b1df344276739ea3bc9344b790821fcd0623e Mon Sep 17 00:00:00 2001 From: Delirium Date: Tue, 21 Nov 2023 11:28:40 +0100 Subject: [PATCH] feat(ui): allow devtools to be resizeable (#1605) --- .../Player/MobilePlayer/PlayerInst.tsx | 45 +- .../Player/ReplayPlayer/PlayerInst.tsx | 59 +- .../BottomBlock/bottomBlock.module.css | 3 +- .../BottomBlock/bottomBlock.module.css | 2 +- .../DevTools/ConsolePanel/ConsolePanel.tsx | 2 +- .../ConsolePanel/MobileConsolePanel.tsx | 6 +- .../DevTools/NetworkPanel/NetworkPanel.tsx | 576 +++++++++--------- .../DevTools/ProfilerPanel/ProfilerPanel.tsx | 6 +- .../StackEventPanel/StackEventPanel.tsx | 2 +- .../shared/DevTools/TimeTable/TimeTable.tsx | 6 - 10 files changed, 397 insertions(+), 310 deletions(-) diff --git a/frontend/app/components/Session/Player/MobilePlayer/PlayerInst.tsx b/frontend/app/components/Session/Player/MobilePlayer/PlayerInst.tsx index 52e3dd381..20d56b3bb 100644 --- a/frontend/app/components/Session/Player/MobilePlayer/PlayerInst.tsx +++ b/frontend/app/components/Session/Player/MobilePlayer/PlayerInst.tsx @@ -26,6 +26,7 @@ import { MobilePlayerContext } from 'App/components/Session/playerContext'; import { MobileStackEventPanel } from 'Shared/DevTools/StackEventPanel'; import ReplayWindow from "Components/Session/Player/MobilePlayer/ReplayWindow"; import PerfWarnings from "Components/Session/Player/MobilePlayer/PerfWarnings"; +import { debounceUpdate, getDefaultPanelHeight } from "Components/Session/Player/ReplayPlayer/PlayerInst"; interface IProps { fullView: boolean; @@ -42,6 +43,8 @@ interface IProps { } function Player(props: IProps) { + const defaultHeight = getDefaultPanelHeight() + const [panelHeight, setPanelHeight] = React.useState(defaultHeight); const { fullscreen, fullscreenOff, @@ -77,6 +80,30 @@ function Player(props: IProps) { if (!playerContext.player) return null; const maxWidth = activeTab ? 'calc(100vw - 270px)' : '100vw'; + + const handleResize = (e: React.MouseEvent) => { + e.preventDefault(); + const startY = e.clientY; + const startHeight = panelHeight; + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + const handleMouseMove = (e: MouseEvent) => { + const deltaY = e.clientY - startY; + const diff = startHeight - deltaY; + const max = diff > window.innerHeight / 2 ? window.innerHeight / 2 : diff; + const newHeight = Math.max(50, max); + setPanelHeight(newHeight); + debounceUpdate(newHeight) + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }; + return (
{!fullscreen && !!bottomBlock && ( -
+
+
{bottomBlock === OVERVIEW && } - {bottomBlock === CONSOLE && } + {bottomBlock === CONSOLE && } {bottomBlock === STACKEVENTS && } - {bottomBlock === NETWORK && } + {bottomBlock === NETWORK && } {bottomBlock === PERFORMANCE && } {bottomBlock === EXCEPTIONS && }
diff --git a/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx b/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx index 70782b394..ceecda380 100644 --- a/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx +++ b/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx @@ -32,6 +32,7 @@ import { OverviewPanel } from 'Components/Session_/OverviewPanel'; import ConsolePanel from 'Shared/DevTools/ConsolePanel'; import ProfilerPanel from 'Shared/DevTools/ProfilerPanel'; import { PlayerContext } from 'App/components/Session/playerContext'; +import { debounce } from 'App/utils'; interface IProps { fullView: boolean; @@ -45,7 +46,23 @@ interface IProps { updateLastPlayedSession: (id: string) => void; } +export const heightKey = 'playerPanelHeight' +export const debounceUpdate = debounce((height: number) => { + localStorage.setItem(heightKey, height.toString()); +}, 500) +export const getDefaultPanelHeight = () => { + const storageHeight = localStorage.getItem(heightKey) + if (storageHeight) { + const height = parseInt(storageHeight, 10) + return height > window.innerHeight / 2 ? window.innerHeight / 2 : height + } else { + return 300 + } +} + function Player(props: IProps) { + const defaultHeight = getDefaultPanelHeight() + const [panelHeight, setPanelHeight] = React.useState(defaultHeight); const { fullscreen, fullscreenOff, nextId, bottomBlock, activeTab, fullView } = props; const playerContext = React.useContext(PlayerContext); const isReady = playerContext.store.get().ready; @@ -69,6 +86,30 @@ function Player(props: IProps) { if (!playerContext.player) return null; const maxWidth = activeTab ? 'calc(100vw - 270px)' : '100vw'; + + const handleResize = (e: React.MouseEvent) => { + e.preventDefault(); + const startY = e.clientY; + const startHeight = panelHeight; + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + const handleMouseMove = (e: MouseEvent) => { + const deltaY = e.clientY - startY; + const diff = startHeight - deltaY; + const max = diff > window.innerHeight / 2 ? window.innerHeight / 2 : diff; + const newHeight = Math.max(50, max); + setPanelHeight(newHeight); + debounceUpdate(newHeight) + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }; + return (
{!fullscreen && !!bottomBlock && ( -
+
+
{bottomBlock === OVERVIEW && } {bottomBlock === CONSOLE && } - {bottomBlock === NETWORK && } + {bottomBlock === NETWORK && } {bottomBlock === STACKEVENTS && } {bottomBlock === STORAGE && } - {bottomBlock === PROFILER && } + {bottomBlock === PROFILER && } {bottomBlock === PERFORMANCE && } {bottomBlock === GRAPHQL && } {bottomBlock === EXCEPTIONS && } diff --git a/frontend/app/components/Session_/BottomBlock/bottomBlock.module.css b/frontend/app/components/Session_/BottomBlock/bottomBlock.module.css index 99bdd42b4..e91227e4b 100644 --- a/frontend/app/components/Session_/BottomBlock/bottomBlock.module.css +++ b/frontend/app/components/Session_/BottomBlock/bottomBlock.module.css @@ -3,7 +3,6 @@ background: $white; /* padding-right: 10px; */ /* border: solid thin $gray-light; */ - height: 300px; - + height: 100%; border-top: thin dashed #cccccc; } diff --git a/frontend/app/components/shared/DevTools/BottomBlock/bottomBlock.module.css b/frontend/app/components/shared/DevTools/BottomBlock/bottomBlock.module.css index 99bdd42b4..937cc27fe 100644 --- a/frontend/app/components/shared/DevTools/BottomBlock/bottomBlock.module.css +++ b/frontend/app/components/shared/DevTools/BottomBlock/bottomBlock.module.css @@ -3,7 +3,7 @@ background: $white; /* padding-right: 10px; */ /* border: solid thin $gray-light; */ - height: 300px; + height: 100%; border-top: thin dashed #cccccc; } diff --git a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx index b56b4ce67..ffaee7ce4 100644 --- a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx +++ b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx @@ -161,7 +161,7 @@ function ConsolePanel({ isLive }: { isLive?: boolean }) { return ( diff --git a/frontend/app/components/shared/DevTools/ConsolePanel/MobileConsolePanel.tsx b/frontend/app/components/shared/DevTools/ConsolePanel/MobileConsolePanel.tsx index d6b398d6c..d6af1d9b8 100644 --- a/frontend/app/components/shared/DevTools/ConsolePanel/MobileConsolePanel.tsx +++ b/frontend/app/components/shared/DevTools/ConsolePanel/MobileConsolePanel.tsx @@ -79,15 +79,11 @@ function MobileConsolePanel() { const { logList, - // exceptionsList, logListNow, exceptionsListNow, } = store.get(); const list = logList as ILog[]; - // useMemo(() => logList.concat(exceptionsList).sort((a, b) => a.time - b.time), - // [ logList.length, exceptionsList.length ], - // ) as ILog[] let filteredList = useRegExListFilterMemo(list, (l) => l.value, filter); filteredList = useTabListFilterMemo(filteredList, (l) => LEVEL_TAB[l.level], ALL, activeTab); @@ -163,7 +159,7 @@ function MobileConsolePanel() { return ( diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx index ade07587c..c8f3fc060 100644 --- a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx +++ b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx @@ -147,7 +147,7 @@ function renderStatus({ status, cached }: { status: string; cached: boolean }) { ); } -function NetworkPanelCont({ startedAt }: { startedAt: number }) { +function NetworkPanelCont({ startedAt, panelHeight }: { startedAt: number; panelHeight: number }) { const { player, store } = React.useContext(PlayerContext); const { domContentLoadedTime, loadTime, domBuildingTime, tabStates, currentTab } = store.get(); @@ -161,6 +161,7 @@ function NetworkPanelCont({ startedAt }: { startedAt: number }) { return ( { - const { showModal } = useModal(); - const [sortBy, setSortBy] = useState('time'); - const [sortAscending, setSortAscending] = useState(true); - const [showOnlyErrors, setShowOnlyErrors] = useState(false); +const NetworkPanelComp = observer( + ({ + loadTime, + domBuildingTime, + domContentLoadedTime, + fetchList, + resourceList, + fetchListNow, + resourceListNow, + player, + startedAt, + isMobile, + panelHeight, + }: Props) => { + const { showModal } = useModal(); + const [sortBy, setSortBy] = useState('time'); + const [sortAscending, setSortAscending] = useState(true); + const [showOnlyErrors, setShowOnlyErrors] = useState(false); - const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); - const { - sessionStore: { devTools }, - } = useStore(); - const filter = devTools[INDEX_KEY].filter; - const activeTab = devTools[INDEX_KEY].activeTab; - const activeIndex = devTools[INDEX_KEY].index; + const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); + const { + sessionStore: { devTools }, + } = useStore(); + const filter = devTools[INDEX_KEY].filter; + const activeTab = devTools[INDEX_KEY].activeTab; + const activeIndex = devTools[INDEX_KEY].index; - const list = useMemo( - () => - // TODO: better merge (with body size info) - do it in player - resourceList - .filter( - (res) => - !fetchList.some((ft) => { - // res.url !== ft.url doesn't work on relative URLs appearing within fetchList (to-fix in player) - if (res.name === ft.name) { - if (res.time === ft.time) return true; - if (res.url.includes(ft.url)) { - return ( - Math.abs(res.time - ft.time) < 350 || - Math.abs(res.timestamp - ft.timestamp) < 350 - ); + const list = useMemo( + () => + // TODO: better merge (with body size info) - do it in player + resourceList + .filter( + (res) => + !fetchList.some((ft) => { + // res.url !== ft.url doesn't work on relative URLs appearing within fetchList (to-fix in player) + if (res.name === ft.name) { + if (res.time === ft.time) return true; + if (res.url.includes(ft.url)) { + return ( + Math.abs(res.time - ft.time) < 350 || + Math.abs(res.timestamp - ft.timestamp) < 350 + ); + } } - } - if (res.name !== ft.name) { - return false; - } - if (Math.abs(res.time - ft.time) > 250) { - return false; - } // TODO: find good epsilons - if (Math.abs(res.duration - ft.duration) > 200) { - return false; - } + if (res.name !== ft.name) { + return false; + } + if (Math.abs(res.time - ft.time) > 250) { + return false; + } // TODO: find good epsilons + if (Math.abs(res.duration - ft.duration) > 200) { + return false; + } - return true; - }) - ) - .concat(fetchList) - .sort((a, b) => a.time - b.time), - [resourceList.length, fetchList.length] - ); - - let filteredList = useMemo(() => { - if (!showOnlyErrors) { - return list; - } - return list.filter((it) => parseInt(it.status) >= 400 || !it.success); - }, [showOnlyErrors, list]); - filteredList = useRegExListFilterMemo( - filteredList, - (it) => [it.status, it.name, it.type], - filter - ); - filteredList = useTabListFilterMemo(filteredList, (it) => TYPE_TO_TAB[it.type], ALL, activeTab); - - const onTabClick = (activeTab: (typeof TAP_KEYS)[number]) => - devTools.update(INDEX_KEY, { activeTab }); - const onFilterChange = ({ target: { value } }: React.ChangeEvent) => - devTools.update(INDEX_KEY, { filter: value }); - - // AutoScroll - const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll( - filteredList, - getLastItemTime(fetchListNow, resourceListNow), - activeIndex, - (index) => devTools.update(INDEX_KEY, { index }) - ); - const onMouseEnter = stopAutoscroll; - const onMouseLeave = () => { - if (isDetailsModalActive) { - return; - } - timeoutStartAutoscroll(); - }; - - const resourcesSize = useMemo( - () => resourceList.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0), - [resourceList.length] - ); - const transferredSize = useMemo( - () => - resourceList.reduce( - (sum, { headerSize, encodedBodySize }) => sum + (headerSize || 0) + (encodedBodySize || 0), - 0 - ), - [resourceList.length] - ); - - 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) => { - setIsDetailsModalActive(true); - showModal( - 0} - />, - { - right: true, - width: 500, - onClose: () => { - setIsDetailsModalActive(false); - timeoutStartAutoscroll(); - }, - } + return true; + }) + ) + .concat(fetchList) + .sort((a, b) => a.time - b.time), + [resourceList.length, fetchList.length] ); - devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) }); - stopAutoscroll(); - }; - return ( - - - -
- Network - {isMobile ? null : - - } -
- -
- -
-
- setShowOnlyErrors(!showOnlyErrors)} - label="4xx-5xx Only" - /> + let filteredList = useMemo(() => { + if (!showOnlyErrors) { + return list; + } + return list.filter((it) => parseInt(it.status) >= 400 || !it.success); + }, [showOnlyErrors, list]); + filteredList = useRegExListFilterMemo( + filteredList, + (it) => [it.status, it.name, it.type], + filter + ); + filteredList = useTabListFilterMemo(filteredList, (it) => TYPE_TO_TAB[it.type], ALL, activeTab); + + const onTabClick = (activeTab: (typeof TAP_KEYS)[number]) => + devTools.update(INDEX_KEY, { activeTab }); + const onFilterChange = ({ target: { value } }: React.ChangeEvent) => + devTools.update(INDEX_KEY, { filter: value }); + + // AutoScroll + const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll( + filteredList, + getLastItemTime(fetchListNow, resourceListNow), + activeIndex, + (index) => devTools.update(INDEX_KEY, { index }) + ); + const onMouseEnter = stopAutoscroll; + const onMouseLeave = () => { + if (isDetailsModalActive) { + return; + } + timeoutStartAutoscroll(); + }; + + const resourcesSize = useMemo( + () => resourceList.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0), + [resourceList.length] + ); + const transferredSize = useMemo( + () => + resourceList.reduce( + (sum, { headerSize, encodedBodySize }) => + sum + (headerSize || 0) + (encodedBodySize || 0), + 0 + ), + [resourceList.length] + ); + + 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) => { + setIsDetailsModalActive(true); + showModal( + 0} + />, + { + right: true, + width: 500, + onClose: () => { + setIsDetailsModalActive(false); + timeoutStartAutoscroll(); + }, + } + ); + devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) }); + stopAutoscroll(); + }; + + return ( + + + +
+ Network + {isMobile ? null : ( + + )}
- - - 0} - /> - 0} - /> - - - - -
- - - No Data + + + +
+
+ setShowOnlyErrors(!showOnlyErrors)} + label="4xx-5xx Only" + />
- } - size="small" - show={filteredList.length === 0} - > - { - devTools.update(INDEX_KEY, { index: filteredList.indexOf(row) }); - player.jump(row.time); - }} - activeIndex={activeIndex} + + + 0} + /> + 0} + /> + + + + +
+ + + No Data +
+ } + size="small" + show={filteredList.length === 0} > - {[ - // { - // label: 'Start', - // width: 120, - // render: renderStart, - // }, - { - label: 'Status', - dataKey: 'status', - width: 90, - render: renderStatus, - }, - { - label: 'Type', - dataKey: 'type', - width: 90, - render: renderType, - }, - { - 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, - }, - ]} - - -
-
-
- ); -}) + {/*@ts-ignore*/} + { + devTools.update(INDEX_KEY, { index: filteredList.indexOf(row) }); + player.jump(row.time); + }} + activeIndex={activeIndex} + > + {[ + // { + // label: 'Start', + // width: 120, + // render: renderStart, + // }, + { + label: 'Status', + dataKey: 'status', + width: 90, + render: renderStatus, + }, + { + label: 'Type', + dataKey: 'type', + width: 90, + render: renderType, + }, + { + 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, + }, + ]} + + + +
+ + ); + } +); const WebNetworkPanel = connect((state: any) => ({ startedAt: state.getIn(['sessions', 'current']).startedAt, @@ -519,7 +528,4 @@ const MobileNetworkPanel = connect((state: any) => ({ startedAt: state.getIn(['sessions', 'current']).startedAt, }))(observer(MobileNetworkPanelCont)); -export { - WebNetworkPanel, - MobileNetworkPanel -} \ No newline at end of file +export { WebNetworkPanel, MobileNetworkPanel }; diff --git a/frontend/app/components/shared/DevTools/ProfilerPanel/ProfilerPanel.tsx b/frontend/app/components/shared/DevTools/ProfilerPanel/ProfilerPanel.tsx index e4a0b2f58..1199dd212 100644 --- a/frontend/app/components/shared/DevTools/ProfilerPanel/ProfilerPanel.tsx +++ b/frontend/app/components/shared/DevTools/ProfilerPanel/ProfilerPanel.tsx @@ -13,7 +13,7 @@ import { useRegExListFilterMemo } from '../useListFilter' const renderDuration = (p: any) => `${p.duration}ms`; const renderName = (p: any) => ; -function ProfilerPanel() { +function ProfilerPanel({ panelHeight }: { panelHeight: number }) { const { store } = React.useContext(PlayerContext) const { tabStates, currentTab } = store.get() const profiles = tabStates[currentTab].profilesList || [] as any[] // TODO lest internal types @@ -26,7 +26,7 @@ function ProfilerPanel() { showModal(, { right: true, width: 500 }); }; return ( - +
Profiler @@ -41,7 +41,7 @@ function ProfilerPanel() { /> - + {[ { label: 'Name', diff --git a/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx b/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx index bb77971d1..8bf4df447 100644 --- a/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx +++ b/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx @@ -139,7 +139,7 @@ function EventsPanel({ return ( diff --git a/frontend/app/components/shared/DevTools/TimeTable/TimeTable.tsx b/frontend/app/components/shared/DevTools/TimeTable/TimeTable.tsx index 3ff1ca54d..63b9ed658 100644 --- a/frontend/app/components/shared/DevTools/TimeTable/TimeTable.tsx +++ b/frontend/app/components/shared/DevTools/TimeTable/TimeTable.tsx @@ -143,12 +143,6 @@ export default class TimeTable extends React.PureComponent { scroller = React.createRef(); autoScroll = true; - // componentDidMount() { - // if (this.scroller.current) { - // this.scroller.current.scrollToRow(this.props.activeIndex); - // } - // } - adjustScroll(prevActiveIndex: number) { if ( this.props.activeIndex &&