ui: redesigned ws panel

This commit is contained in:
nick-delirium 2024-09-25 15:06:11 +02:00
parent 71caeb8ac0
commit e5fd1b235e
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
5 changed files with 179 additions and 14 deletions

View file

@ -17,6 +17,7 @@ import { formatBytes } from 'App/utils';
import { Icon, Input, NoContent, Tabs, Toggler, Tooltip } from 'UI'; import { Icon, Input, NoContent, Tabs, Toggler, Tooltip } from 'UI';
import FetchDetailsModal from 'Shared/FetchDetailsModal'; import FetchDetailsModal from 'Shared/FetchDetailsModal';
import { WsChannel } from "App/player/web/messages";
import BottomBlock from '../BottomBlock'; import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine'; import InfoLine from '../BottomBlock/InfoLine';
@ -24,6 +25,7 @@ import TimeTable from '../TimeTable';
import useAutoscroll, { getLastItemTime } from '../useAutoscroll'; import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'; import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
import WSModal from './WSModal'; import WSModal from './WSModal';
import WSPanel from './WSPanel';
const INDEX_KEY = 'network'; const INDEX_KEY = 'network';
@ -191,7 +193,6 @@ function NetworkPanelCont({
zoomEndTs: number; zoomEndTs: number;
}) { }) {
const { player, store } = React.useContext(PlayerContext); const { player, store } = React.useContext(PlayerContext);
const { const {
domContentLoadedTime, domContentLoadedTime,
loadTime, loadTime,
@ -334,9 +335,8 @@ export const NetworkPanelComp = observer(
activeOutsideIndex, activeOutsideIndex,
isSpot, isSpot,
}: Props) => { }: Props) => {
const [selectedWsChannel, setSelectedWsChannel] = React.useState<WsChannel[] | null>(null)
const { showModal } = useModal(); const { showModal } = useModal();
const [sortBy, setSortBy] = useState('time');
const [sortAscending, setSortAscending] = useState(true);
const [showOnlyErrors, setShowOnlyErrors] = useState(false); const [showOnlyErrors, setShowOnlyErrors] = useState(false);
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
@ -491,13 +491,10 @@ export const NetworkPanelComp = observer(
const showDetailsModal = (item: any) => { const showDetailsModal = (item: any) => {
if (item.type === 'websocket') { if (item.type === 'websocket') {
const socketMsgList = websocketList.filter( const socketMsgList = websocketList.filter(
(ws) => ws.channelName === item.channelName (ws) => ws.channelName === item.channelName
); );
return showModal(<WSModal socketMsgList={socketMsgList} />, { return setSelectedWsChannel(socketMsgList)
right: true,
width: 700,
});
} }
setIsDetailsModalActive(true); setIsDetailsModalActive(true);
showModal( showModal(
@ -617,8 +614,8 @@ export const NetworkPanelComp = observer(
referenceLines={referenceLines} referenceLines={referenceLines}
renderPopup renderPopup
onRowClick={showDetailsModal} onRowClick={showDetailsModal}
sortBy={sortBy} sortBy={'time'}
sortAscending={sortAscending} sortAscending
onJump={(row: any) => { onJump={(row: any) => {
devTools.update(INDEX_KEY, { devTools.update(INDEX_KEY, {
index: filteredList.indexOf(row), index: filteredList.indexOf(row),
@ -671,6 +668,9 @@ export const NetworkPanelComp = observer(
}, },
]} ]}
</TimeTable> </TimeTable>
{selectedWsChannel ? (
<WSPanel socketMsgList={selectedWsChannel} onClose={() => setSelectedWsChannel(null)} />
) : null}
</NoContent> </NoContent>
</BottomBlock.Content> </BottomBlock.Content>
</BottomBlock> </BottomBlock>

View file

@ -0,0 +1,165 @@
import { Timed } from 'Player';
import React from 'react';
import { durationFromMs } from 'App/date';
import { filterList } from 'App/utils';
import { CopyButton, Icon, Input } from 'UI';
type SocketMsg = Timed & {
channelName: string;
data: string;
timestamp: number;
dir: 'up' | 'down';
messageType: string;
};
interface Props {
socketMsgList: Array<SocketMsg>;
onClose: () => void;
}
const lineLength = 40;
function WSPanel({ socketMsgList, onClose }: Props) {
const [query, setQuery] = React.useState('');
const [list, setList] = React.useState(socketMsgList);
const [selectedRow, setSelectedRow] = React.useState<SocketMsg | null>(null);
const onQueryChange = (e) => {
setQuery(e.target.value);
const newList = filterList(socketMsgList, e.target.value, [
'data',
'messageType',
]);
setList(newList);
};
return (
<div className={'h-full w-3/4 absolute top-0 right-0 bg-white border z-10'}>
<div className={'flex items-center p-2 w-full gap-2'}>
<Icon
name={'close'}
size={16}
onClick={onClose}
className={'cursor-pointer'}
/>
<div>{socketMsgList[0].channelName}</div>
<div className={'ml-auto'}>
<Input
className="input-small"
placeholder="Filter by name, type, method or value"
icon="search"
name="filter"
onChange={onQueryChange}
height={28}
width={280}
value={query}
/>
</div>
</div>
<div className={'grid grid-cols-12 font-semibold border-b px-4 py-2'}>
<div className={'col-span-9 flex items-center gap-2'}>Data</div>
<div className={'col-span-1'}>Length</div>
<div className={'col-span-2 text-right'}>Time</div>
</div>
<div
style={{
height: 'calc(100% - 78px)',
width: '100%',
overflowY: 'auto',
position: 'relative',
}}
>
{list.map((msg) => (
<Row
msg={msg}
key={msg.timestamp}
onSelect={() => setSelectedRow(msg)}
/>
))}
{selectedRow ? (
<SelectedRow msg={selectedRow} onClose={() => setSelectedRow(null)} />
) : null}
</div>
</div>
);
}
function SelectedRow({
msg,
onClose,
}: {
msg: SocketMsg;
onClose: () => void;
}) {
const content = React.useMemo(() => {
return JSON.stringify(msg, null, 2);
}, []);
return (
<div
className={
'absolute bottom-0 left-0 h-3/4 w-full flex flex-col bg-white border-t border-gray-lighter'
}
>
<div className={'flex gap-2 items-center p-2'}>
<Icon
name={'close'}
size={16}
onClick={onClose}
className={'cursor-pointer'}
/>
<span>{msg.messageType}</span>
<div className={'ml-auto'}>
<CopyButton content={content} />
</div>
</div>
<div className={'border-t border-gray-lighter bg-gray-lightest p-4'}>{msg.data}</div>
</div>
);
}
function MsgDirection({ dir }: { dir: 'up' | 'down' }) {
return (
<Icon
name={dir === 'up' ? 'arrow-up' : 'arrow-down'}
size="12"
color={dir === 'up' ? 'red' : 'main'}
/>
);
}
function Row({ msg, onSelect }: { msg: SocketMsg; onSelect: () => void }) {
return (
<>
<div
className={'border-b grid grid-cols-12 hover:bg-active-blue cursor-pointer'}
onClick={onSelect}
>
<div className={'col-span-9 flex items-center gap-2 p-2'}>
<MsgDirection dir={msg.dir} />
<span className={'bg-active-blue px-2 py-1'}>{msg.messageType}</span>
<span
className={'overflow-hidden text-ellipsis whitespace-nowrap'}
style={{ maxHeight: 44 }}
>
{msg.data}
</span>
{msg.data.length > lineLength ? (
<div
className={
'rounded-full font-bold text-xl p-2 bg-white w-6 h-6 flex items-center justify-center'
}
>
<span>{isOpen ? '-' : '+'}</span>
</div>
) : null}
</div>
<div className={'col-span-1 p-2'}>{msg.data.length}</div>
<div className={'col-span-2 p-2 text-right'}>
{durationFromMs(msg.time, true)}
</div>
</div>
</>
);
}
export default WSPanel;

View file

@ -40,7 +40,7 @@ $offset: 10px;
width: 1px; width: 1px;
} }
scrollbar-width: thin; scrollbar-width: thin;
font-size: 12px; font-size: 14px;
font-family: 'Menlo', 'monaco', 'consolas', monospace; font-family: 'Menlo', 'monaco', 'consolas', monospace;
} }

View file

@ -13,6 +13,7 @@ interface IProps {
style?: object style?: object
marginRight?: number marginRight?: number
inline?: boolean inline?: boolean
onClick?: () => void
} }
const Icon: React.FunctionComponent<IProps> = ({ const Icon: React.FunctionComponent<IProps> = ({

View file

@ -87,10 +87,9 @@ export const filterList = <T extends Record<string, any>>(
): T[] => { ): T[] => {
if (searchQuery === '') return list; if (searchQuery === '') return list;
const filterRE = getRE(searchQuery, 'i'); const filterRE = getRE(searchQuery, 'i');
let _list = list.filter((listItem: T) => { return list.filter((listItem: T) => {
return testKeys.some((key) => filterRE.test(listItem[key])) || searchCb?.(listItem, filterRE); return testKeys.some((key) => filterRE.test(listItem[key])) || searchCb?.(listItem, filterRE);
}); });
return _list;
}; };
export const getStateColor = (state) => { export const getStateColor = (state) => {