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 { 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>
|
||||||
|
|
|
||||||
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;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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> = ({
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue