ui: redesigned ws panel
This commit is contained in:
parent
71caeb8ac0
commit
e5fd1b235e
5 changed files with 179 additions and 14 deletions
|
|
@ -17,6 +17,7 @@ import { formatBytes } from 'App/utils';
|
|||
import { Icon, Input, NoContent, Tabs, Toggler, Tooltip } from 'UI';
|
||||
|
||||
import FetchDetailsModal from 'Shared/FetchDetailsModal';
|
||||
import { WsChannel } from "App/player/web/messages";
|
||||
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import InfoLine from '../BottomBlock/InfoLine';
|
||||
|
|
@ -24,6 +25,7 @@ import TimeTable from '../TimeTable';
|
|||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
||||
import WSModal from './WSModal';
|
||||
import WSPanel from './WSPanel';
|
||||
|
||||
const INDEX_KEY = 'network';
|
||||
|
||||
|
|
@ -191,7 +193,6 @@ function NetworkPanelCont({
|
|||
zoomEndTs: number;
|
||||
}) {
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
|
||||
const {
|
||||
domContentLoadedTime,
|
||||
loadTime,
|
||||
|
|
@ -334,9 +335,8 @@ export const NetworkPanelComp = observer(
|
|||
activeOutsideIndex,
|
||||
isSpot,
|
||||
}: Props) => {
|
||||
const [selectedWsChannel, setSelectedWsChannel] = React.useState<WsChannel[] | null>(null)
|
||||
const { showModal } = useModal();
|
||||
const [sortBy, setSortBy] = useState('time');
|
||||
const [sortAscending, setSortAscending] = useState(true);
|
||||
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
|
||||
|
||||
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
|
||||
|
|
@ -491,13 +491,10 @@ export const NetworkPanelComp = observer(
|
|||
const showDetailsModal = (item: any) => {
|
||||
if (item.type === 'websocket') {
|
||||
const socketMsgList = websocketList.filter(
|
||||
(ws) => ws.channelName === item.channelName
|
||||
);
|
||||
(ws) => ws.channelName === item.channelName
|
||||
);
|
||||
|
||||
return showModal(<WSModal socketMsgList={socketMsgList} />, {
|
||||
right: true,
|
||||
width: 700,
|
||||
});
|
||||
return setSelectedWsChannel(socketMsgList)
|
||||
}
|
||||
setIsDetailsModalActive(true);
|
||||
showModal(
|
||||
|
|
@ -617,8 +614,8 @@ export const NetworkPanelComp = observer(
|
|||
referenceLines={referenceLines}
|
||||
renderPopup
|
||||
onRowClick={showDetailsModal}
|
||||
sortBy={sortBy}
|
||||
sortAscending={sortAscending}
|
||||
sortBy={'time'}
|
||||
sortAscending
|
||||
onJump={(row: any) => {
|
||||
devTools.update(INDEX_KEY, {
|
||||
index: filteredList.indexOf(row),
|
||||
|
|
@ -671,6 +668,9 @@ export const NetworkPanelComp = observer(
|
|||
},
|
||||
]}
|
||||
</TimeTable>
|
||||
{selectedWsChannel ? (
|
||||
<WSPanel socketMsgList={selectedWsChannel} onClose={() => setSelectedWsChannel(null)} />
|
||||
) : null}
|
||||
</NoContent>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
|
|
|
|||
165
frontend/app/components/shared/DevTools/NetworkPanel/WSPanel.tsx
Normal file
165
frontend/app/components/shared/DevTools/NetworkPanel/WSPanel.tsx
Normal 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;
|
||||
|
|
@ -40,7 +40,7 @@ $offset: 10px;
|
|||
width: 1px;
|
||||
}
|
||||
scrollbar-width: thin;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
font-family: 'Menlo', 'monaco', 'consolas', monospace;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ interface IProps {
|
|||
style?: object
|
||||
marginRight?: number
|
||||
inline?: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const Icon: React.FunctionComponent<IProps> = ({
|
||||
|
|
|
|||
|
|
@ -87,10 +87,9 @@ export const filterList = <T extends Record<string, any>>(
|
|||
): T[] => {
|
||||
if (searchQuery === '') return list;
|
||||
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 _list;
|
||||
};
|
||||
|
||||
export const getStateColor = (state) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue