Player improvs (#2835)
* ui: fix performance bottlenecks, split data sources in devtools panes * ui: move xray warn * Player ux improvements (#2834) * Player UX improvements. DevTools (Including multi-tab) Actions panel (User events, Click maps, Tag Elements) * ui: remove unused imports, remove str templ classnames --------- Co-authored-by: Sudheer Salavadi <connect.uxmaster@gmail.com> --------- Co-authored-by: Sudheer Salavadi <connect.uxmaster@gmail.com>
This commit is contained in:
parent
c963ec5e91
commit
38594319f0
46 changed files with 814 additions and 483 deletions
|
|
@ -9,7 +9,6 @@ import {
|
|||
import React from 'react';
|
||||
|
||||
import ExCard from './ExCard';
|
||||
import { size } from '@floating-ui/react-dom-interactions';
|
||||
|
||||
const TYPES = {
|
||||
Frustrations: 'frustrations',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useStore } from 'App/mstore';
|
||||
import React from 'react';
|
||||
// import Select from 'Shared/Select';
|
||||
import { Select } from 'antd';
|
||||
|
||||
const sortOptions = [
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
import { useStore } from 'App/mstore';
|
||||
import { session as sessionRoute, withSiteId } from 'App/routes';
|
||||
import { SummaryButton } from 'Components/Session_/Player/Controls/Controls';
|
||||
import { MobEventsList, WebEventsList } from "../../../Session_/Player/Controls/EventsList";
|
||||
import useShortcuts from '../ReplayPlayer/useShortcuts';
|
||||
|
||||
export const SKIP_INTERVALS = {
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ function PlayerBlockHeader(props: any) {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative border-l border-l-gray-lighter" style={{ minWidth: '270px' }}>
|
||||
<div className="px-2 relative border-l border-l-gray-lighter" style={{ minWidth: '270px' }}>
|
||||
<Tabs
|
||||
tabs={TABS}
|
||||
active={activeTab}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Segmented } from 'antd';
|
||||
import React from 'react';
|
||||
import { VList, VListHandle } from 'virtua';
|
||||
import { PlayerContext } from "App/components/Session/playerContext";
|
||||
|
|
@ -12,7 +11,9 @@ import {
|
|||
} from 'App/components/Client/Integrations/apiMethods';
|
||||
import BottomBlock from 'App/components/shared/DevTools/BottomBlock';
|
||||
import { capitalize } from 'App/utils';
|
||||
import { Icon, Input } from 'UI';
|
||||
import { Icon } from 'UI';
|
||||
import { Segmented, Input, Tooltip } from 'antd';
|
||||
import {SearchOutlined} from '@ant-design/icons';
|
||||
import { client } from 'App/mstore';
|
||||
import { FailedFetch, LoadingFetch } from "./StatusMessages";
|
||||
import {
|
||||
|
|
@ -82,25 +83,44 @@ function BackendLogsPanel() {
|
|||
return (
|
||||
<BottomBlock style={{ height: '100%' }}>
|
||||
<BottomBlock.Header>
|
||||
<div className={'flex gap-2 items-center w-full'}>
|
||||
<div className={'font-semibold'}>Traces</div>
|
||||
{tabs.length && tab ? (
|
||||
<div>
|
||||
<Segmented options={tabs} value={tab} onChange={setTab} />
|
||||
<div className='flex items-center justify-between w-full'>
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<div className={'font-semibold'}>Traces</div>
|
||||
{tabs.length && tab ? (
|
||||
<div>
|
||||
<Segmented options={tabs} value={tab} onChange={setTab} size='small' />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className={'ml-auto'} />
|
||||
<Input
|
||||
className="input-small h-8"
|
||||
placeholder="Filter by keyword"
|
||||
icon="search"
|
||||
name="filter"
|
||||
height={28}
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
<div className='flex items-center gap-2'>
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: 'All Tabs', value: 'all', },
|
||||
{ label: (
|
||||
<Tooltip title="Backend logs are fetched for all tabs combined.">
|
||||
<span>Current Tab</span>
|
||||
</Tooltip>),
|
||||
value: 'current', disabled: true},
|
||||
]}
|
||||
defaultValue="all"
|
||||
size="small"
|
||||
className="rounded-full font-medium"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<Input
|
||||
className="rounded-lg"
|
||||
placeholder="Filter by keyword"
|
||||
name="filter"
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
size='small'
|
||||
prefix={<SearchOutlined className='text-neutral-400' />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
|
||||
<BottomBlock.Content className="overflow-y-auto">
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export function LoadingFetch({ provider }: { provider: string }) {
|
|||
'w-full h-full flex items-center justify-center flex-col gap-2'
|
||||
}
|
||||
>
|
||||
<LoadingOutlined style={{ fontSize: 32 }} />
|
||||
<LoadingOutlined size={32} />
|
||||
<div>Fetching logs from {provider}...</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -33,16 +33,23 @@ export function FailedFetch({
|
|||
'w-full h-full flex flex-col items-center justify-center gap-2'
|
||||
}
|
||||
>
|
||||
<Icon name={'exclamation-circle'} size={32} />
|
||||
<div className={'flex items-center gap-1'}>
|
||||
|
||||
<div className={'flex items-center gap-1 font-medium'}>
|
||||
<Icon name={'exclamation-circle'} size={14} />
|
||||
<span>Failed to fetch logs from {provider}. </span>
|
||||
<div className={'link'} onClick={onRetry}>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-3'>
|
||||
|
||||
<Button type='text' size='small' onClick={onRetry}>
|
||||
Retry
|
||||
</div>
|
||||
</div>
|
||||
<div className={'link'} onClick={() => history.push(intPath)}>
|
||||
</Button>
|
||||
|
||||
<Button type='text' size='small' onClick={() => history.push(intPath)}>
|
||||
Check Configuration
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,51 @@ import { useStore } from 'App/mstore';
|
|||
import SaveModal from 'Components/Session/Player/TagWatch/SaveModal';
|
||||
import React from 'react';
|
||||
import { PlayerContext } from 'Components/Session/playerContext';
|
||||
import { Button, Input } from 'antd';
|
||||
import { CopyButton } from 'UI';
|
||||
import { SearchOutlined, ZoomInOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, Tooltip } from 'antd';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { ZoomInOutlined } from '@ant-design/icons';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { toast } from 'react-toastify';
|
||||
import { FilterKey } from "App/types/filter/filterType";
|
||||
import { addOptionsToFilter } from "App/types/filter/newFilter";
|
||||
import { FilterKey } from 'App/types/filter/filterType';
|
||||
import { addOptionsToFilter } from 'App/types/filter/newFilter';
|
||||
|
||||
interface CopyableTextAreaProps {
|
||||
selector: string;
|
||||
setSelector: (value: string) => void;
|
||||
}
|
||||
|
||||
const CopyableTextArea: React.FC<CopyableTextAreaProps> = ({ selector, setSelector }) => {
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(selector);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full relative">
|
||||
<Input.TextArea
|
||||
value={selector}
|
||||
onChange={(e) => setSelector(e.target.value)}
|
||||
className="rounded-lg font-mono text-sm placeholder:font-sans placeholder:text-base placeholder:text-gray-400"
|
||||
rows={4}
|
||||
style={{ paddingRight: '40px' }}
|
||||
placeholder='Enter selector to tag elements. E.g. .btn-primary'
|
||||
/>
|
||||
<Tooltip title="Copy">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={handleCopy}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '8px',
|
||||
right: '8px',
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function TagWatch() {
|
||||
const { tagWatchStore, searchStore } = useStore();
|
||||
|
|
@ -50,7 +87,7 @@ function TagWatch() {
|
|||
ignoreClickRage: ignoreClRage,
|
||||
ignoreDeadClick: ignoreDeadCl,
|
||||
});
|
||||
const tags = await tagWatchStore.getTags()
|
||||
const tags = await tagWatchStore.getTags();
|
||||
if (tags) {
|
||||
addOptionsToFilter(
|
||||
FilterKey.TAGGED_ELEMENT,
|
||||
|
|
@ -58,42 +95,41 @@ function TagWatch() {
|
|||
);
|
||||
searchStore.refreshFilterOptions();
|
||||
}
|
||||
// @ts-ignore
|
||||
toast.success('Tag created');
|
||||
setSelector('');
|
||||
return tag
|
||||
return tag;
|
||||
} catch {
|
||||
// @ts-ignore
|
||||
toast.error('Failed to create tag');
|
||||
}
|
||||
};
|
||||
|
||||
const openSaveModal = () => {
|
||||
if (selector === '') {
|
||||
return;
|
||||
}
|
||||
showModal(<SaveModal onSave={onSave} hideModal={hideModal} />, { right: true, width: 400 });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'w-full h-full p-2 flex flex-col gap-2'}>
|
||||
<div className={'flex items-center justify-between'}>
|
||||
<div className={'font-semibold text-xl'}>Element Selector</div>
|
||||
<CopyButton content={selector} />
|
||||
<div className="w-full h-full p-4 flex flex-col gap-2">
|
||||
<div className="flex flex-col items-center justify-between">
|
||||
<p>Select elements in the session play area to tag by class selector and filter sessions to verify their rendering.</p>
|
||||
|
||||
</div>
|
||||
<Input.TextArea value={selector} onChange={(e) => setSelector(e.target.value)} />
|
||||
|
||||
<CopyableTextArea selector={selector} setSelector={setSelector} />
|
||||
|
||||
<Button
|
||||
onClick={openSaveModal}
|
||||
type={'primary'}
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<ZoomInOutlined />}
|
||||
disabled={selector === ''}
|
||||
>
|
||||
Tag Element
|
||||
</Button>
|
||||
<div className={'text-disabled-text text-sm'}>
|
||||
Create and filter sessions by ‘watch elements’ to determine if they rendered or not.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(TagWatch);
|
||||
export default observer(TagWatch);
|
||||
|
|
@ -18,7 +18,7 @@ function RightBlock({
|
|||
switch (activeTab) {
|
||||
case 'EVENTS':
|
||||
return (
|
||||
<div className={cn('flex flex-col bg-white border-l', stl.panel)}>
|
||||
<div className={cn('flex flex-col border-l', stl.panel)}>
|
||||
<EventsBlock setActiveTab={setActiveTab} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ const Tabs = ({ tabs, active, onClick, border = true, className }: Props) => {
|
|||
return (
|
||||
<div className={cn(stl.tabs, className, { [stl.bordered]: border })} role="tablist">
|
||||
<Segmented
|
||||
className='w-full'
|
||||
size="small"
|
||||
value={active}
|
||||
options={tabs.map(({ key, text, hidden = false, disabled = false, iconComp = null }) => ({
|
||||
label: (
|
||||
|
|
@ -29,14 +31,14 @@ const Tabs = ({ tabs, active, onClick, border = true, className }: Props) => {
|
|||
onClick={() => {
|
||||
onClick(key);
|
||||
}}
|
||||
className={'font-semibold flex gap-1 items-center'}
|
||||
className={'font-medium flex gap-1 items-center hover:text-teal rounded-lg'}
|
||||
>
|
||||
{iconComp ? iconComp : <Icon size={16} color={'black'} name={iconMap[key as keyof typeof iconMap]} />}
|
||||
{iconComp ? iconComp : <Icon size={14} color="currentColor" style={{ fill: 'currentColor', strokeWidth:'0' }} name={iconMap[key as keyof typeof iconMap]} />}
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
),
|
||||
value: key,
|
||||
disabled: disabled,
|
||||
disabled: disabled,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ const Event: React.FC<Props> = ({
|
|||
>
|
||||
<div className={cn(cls.main, 'flex flex-col w-full')}>
|
||||
<div
|
||||
className={cn('flex items-center w-full', { 'px-4': isLocation })}
|
||||
className={cn('flex items-start w-full', { 'px-4': isLocation })}
|
||||
>
|
||||
<div style={{ minWidth: '16px' }}>
|
||||
{event.type && iconName ? (
|
||||
|
|
@ -169,20 +169,18 @@ const Event: React.FC<Props> = ({
|
|||
)}
|
||||
</div>
|
||||
<div className="ml-3 w-full">
|
||||
<div className="flex w-full items-first justify-between">
|
||||
<div className="flex w-full items-start">
|
||||
<div
|
||||
className="flex items-center w-full"
|
||||
className="flex flex-col justify-center items-start w-full"
|
||||
style={{ minWidth: '0' }}
|
||||
>
|
||||
<span
|
||||
className={cn(cls.title, { 'font-medium': isLocation })}
|
||||
>
|
||||
<span className={cn(cls.title, 'font-medium')}>
|
||||
{title}
|
||||
</span>
|
||||
{body && !isLocation && (
|
||||
<TextEllipsis
|
||||
maxWidth="60%"
|
||||
className="w-full ml-2 text-sm color-gray-medium"
|
||||
maxWidth="80%"
|
||||
className="w-full text-sm color-gray-medium"
|
||||
text={body}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -202,8 +200,7 @@ const Event: React.FC<Props> = ({
|
|||
{isLocation && (
|
||||
<div className="pt-1 px-4">
|
||||
<TextEllipsis
|
||||
maxWidth="80%"
|
||||
className="text-sm font-normal color-gray-medium"
|
||||
className="text-sm ms-8 font-normal color-gray-medium"
|
||||
text={body}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Icon, TextEllipsis } from 'UI';
|
|||
import Event from './Event';
|
||||
import NoteEvent from './NoteEvent';
|
||||
import stl from './eventGroupWrapper.module.css';
|
||||
import cn from 'classnames'
|
||||
|
||||
function EventGroupWrapper(props) {
|
||||
const { userStore } = useStore();
|
||||
|
|
@ -132,7 +133,7 @@ function EventGroupWrapper(props) {
|
|||
{isFirst && isLocation && event.referrer && (
|
||||
<TextEllipsis>
|
||||
<div className={stl.referrer}>
|
||||
Referrer: <span className={stl.url}>{safeRef}</span>
|
||||
Referrer: <span className={cn(stl.url, '!font-normal')}>{safeRef}</span>
|
||||
</div>
|
||||
</TextEllipsis>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React from 'react';
|
||||
import { Input, Button } from 'UI';
|
||||
import {Input, Button, Tooltip} from 'antd';
|
||||
import {CloseOutlined, SearchOutlined} from '@ant-design/icons';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
|
||||
function EventSearch(props) {
|
||||
const { player } = React.useContext(PlayerContext);
|
||||
|
||||
const { onChange, value, header, setActiveTab } = props;
|
||||
const { onChange, value, header, setActiveTab, eventsText } = props;
|
||||
|
||||
const toggleEvents = () => player.toggleEvents();
|
||||
|
||||
|
|
@ -16,25 +17,25 @@ function EventSearch(props) {
|
|||
<Input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Filter"
|
||||
className="inset-0 w-full"
|
||||
placeholder={`Filter ${eventsText}`}
|
||||
className="w-full rounded-lg"
|
||||
name="query"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
wrapperClassName="w-full"
|
||||
style={{ height: '32px' }}
|
||||
autoComplete="off chromebugfix"
|
||||
prefix={<SearchOutlined />}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="ml-2"
|
||||
icon="close"
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
setActiveTab('');
|
||||
toggleEvents();
|
||||
}}
|
||||
/>
|
||||
<Tooltip title="Close Panel" placement='bottom' >
|
||||
<Button
|
||||
className="ml-2"
|
||||
type='text'
|
||||
onClick={() => {
|
||||
setActiveTab('');
|
||||
toggleEvents();
|
||||
}}
|
||||
icon={<CloseOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ function EventsBlock(props: IProps) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className={cn(styles.header, 'p-4')}>
|
||||
<div className={cn(styles.header, 'py-4 px-2 bg-gradient-to-t from-transparent to-neutral-50 h-[57px]' )}>
|
||||
{uxtestingStore.isUxt() ? (
|
||||
<div style={{ width: 240, height: 130 }} className={'relative'}>
|
||||
<video
|
||||
|
|
@ -219,14 +219,14 @@ function EventsBlock(props: IProps) {
|
|||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={cn(styles.hAndProgress, 'mt-3')}>
|
||||
<div className={cn(styles.hAndProgress, 'mt-0')}>
|
||||
<EventSearch
|
||||
onChange={write}
|
||||
setActiveTab={setActiveTab}
|
||||
value={query}
|
||||
eventsText={usedEvents.length ? `${usedEvents.length} Events` : '0 Events'}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 color-gray-medium">{eventsText}</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn('flex-1 pb-4', styles.eventsList)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Segmented } from 'antd';
|
||||
import {InfoCircleOutlined} from '@ant-design/icons'
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useEffect } from 'react';
|
||||
|
|
@ -12,6 +13,7 @@ import SummaryBlock from 'Components/Session/Player/ReplayPlayer/SummaryBlock';
|
|||
import { SummaryButton } from 'Components/Session_/Player/Controls/Controls';
|
||||
import TimelineZoomButton from 'Components/Session_/Player/Controls/components/TimelineZoomButton';
|
||||
import { Icon, NoContent } from 'UI';
|
||||
import TabSelector from "../../shared/DevTools/TabSelector";
|
||||
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import EventRow from './components/EventRow';
|
||||
|
|
@ -133,17 +135,66 @@ function WebOverviewPanelCont() {
|
|||
'ERRORS',
|
||||
'NETWORK',
|
||||
]);
|
||||
const globalTabs = ['FRUSTRATIONS', 'ERRORS']
|
||||
|
||||
const { endTime, currentTab, tabStates } = store.get();
|
||||
|
||||
const stackEventList = tabStates[currentTab]?.stackList || [];
|
||||
const frustrationsList = tabStates[currentTab]?.frustrationsList || [];
|
||||
const exceptionsList = tabStates[currentTab]?.exceptionsList || [];
|
||||
const resourceListUnmap = tabStates[currentTab]?.resourceList || [];
|
||||
const fetchList = tabStates[currentTab]?.fetchList || [];
|
||||
const graphqlList = tabStates[currentTab]?.graphqlList || [];
|
||||
const performanceChartData =
|
||||
tabStates[currentTab]?.performanceChartData || [];
|
||||
const tabValues = Object.values(tabStates);
|
||||
const dataSource = uiPlayerStore.dataSource;
|
||||
const showSingleTab = dataSource === 'current';
|
||||
|
||||
const {
|
||||
stackEventList = [],
|
||||
frustrationsList = [],
|
||||
exceptionsList = [],
|
||||
resourceListUnmap = [],
|
||||
fetchList = [],
|
||||
graphqlList = [],
|
||||
performanceChartData = [],
|
||||
} = React.useMemo(() => {
|
||||
if (showSingleTab) {
|
||||
const stackEventList = tabStates[currentTab].stackList;
|
||||
const frustrationsList = tabStates[currentTab].frustrationsList;
|
||||
const exceptionsList = tabStates[currentTab].exceptionsList;
|
||||
const resourceListUnmap = tabStates[currentTab].resourceList;
|
||||
const fetchList = tabStates[currentTab].fetchList;
|
||||
const graphqlList = tabStates[currentTab].graphqlList;
|
||||
const performanceChartData =
|
||||
tabStates[currentTab].performanceChartData;
|
||||
|
||||
return {
|
||||
stackEventList,
|
||||
frustrationsList,
|
||||
exceptionsList,
|
||||
resourceListUnmap,
|
||||
fetchList,
|
||||
graphqlList,
|
||||
performanceChartData,
|
||||
}
|
||||
} else {
|
||||
const stackEventList = tabValues.flatMap((tab) => tab.stackList);
|
||||
// these two are global
|
||||
const frustrationsList = tabValues[0].frustrationsList;
|
||||
const exceptionsList = tabValues[0].exceptionsList;
|
||||
// we can't compute global chart data because some tabs coexist
|
||||
const performanceChartData: any = [];
|
||||
const resourceListUnmap = tabValues.flatMap((tab) => tab.resourceList);
|
||||
const fetchList = tabValues.flatMap((tab) => tab.fetchList);
|
||||
const graphqlList = tabValues.flatMap((tab) => tab.graphqlList);
|
||||
|
||||
return {
|
||||
stackEventList,
|
||||
frustrationsList,
|
||||
exceptionsList,
|
||||
resourceListUnmap,
|
||||
fetchList,
|
||||
graphqlList,
|
||||
performanceChartData,
|
||||
}
|
||||
}
|
||||
}, [tabStates, currentTab, dataSource, tabValues]);
|
||||
|
||||
console.log(showSingleTab, frustrationsList, performanceChartData);
|
||||
|
||||
const fetchPresented = fetchList.length > 0;
|
||||
const resourceList = resourceListUnmap
|
||||
|
|
@ -168,7 +219,18 @@ function WebOverviewPanelCont() {
|
|||
PERFORMANCE: checkInZoomRange(performanceChartData),
|
||||
FRUSTRATIONS: checkInZoomRange(frustrationsList),
|
||||
};
|
||||
}, [tabStates, currentTab, zoomEnabled, zoomStartTs, zoomEndTs]);
|
||||
}, [
|
||||
tabStates,
|
||||
currentTab,
|
||||
zoomEnabled,
|
||||
zoomStartTs,
|
||||
zoomEndTs,
|
||||
resourceList.length,
|
||||
exceptionsList.length,
|
||||
stackEventList.length,
|
||||
performanceChartData.length,
|
||||
frustrationsList.length,
|
||||
]);
|
||||
|
||||
const originStr = window.env.ORIGIN || window.location.origin;
|
||||
const isSaas = /app\.openreplay\.com/.test(originStr);
|
||||
|
|
@ -187,6 +249,7 @@ function WebOverviewPanelCont() {
|
|||
sessionId={sessionId}
|
||||
setZoomTab={setZoomTab}
|
||||
zoomTab={zoomTab}
|
||||
showSingleTab={showSingleTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -238,6 +301,7 @@ function PanelComponent({
|
|||
spotTime,
|
||||
spotEndTime,
|
||||
onClose,
|
||||
showSingleTab,
|
||||
}: any) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
|
@ -280,12 +344,13 @@ function PanelComponent({
|
|||
) : null}
|
||||
</div>
|
||||
{isSpot ? null : (
|
||||
<div className="flex items-center h-20 mr-4 gap-2">
|
||||
<TimelineZoomButton />
|
||||
<div className="flex items-center h-20 mr-4 gap-3">
|
||||
<FeatureSelection
|
||||
list={selectedFeatures}
|
||||
updateList={setSelectedFeatures}
|
||||
/>
|
||||
<TabSelector />
|
||||
<TimelineZoomButton />
|
||||
</div>
|
||||
)}
|
||||
</BottomBlock.Header>
|
||||
|
|
@ -302,7 +367,7 @@ function PanelComponent({
|
|||
style={{ height: '60px', minHeight: 'unset', padding: 0 }}
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
<InfoCircleOutlined size={18} />
|
||||
Select a debug option to visualize on timeline.
|
||||
</div>
|
||||
}
|
||||
|
|
@ -318,6 +383,7 @@ function PanelComponent({
|
|||
<EventRow
|
||||
isGraph={feature === 'PERFORMANCE'}
|
||||
title={feature}
|
||||
disabled={!showSingleTab}
|
||||
list={resources[feature]}
|
||||
renderElement={(pointer: any[], isGrouped: boolean) => (
|
||||
<TimelinePointer
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { getTimelinePosition } from 'App/utils';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import { Icon } from 'UI';
|
||||
import { InfoCircleOutlined} from '@ant-design/icons'
|
||||
import {Tooltip} from 'antd';
|
||||
import PerformanceGraph from '../PerformanceGraph';
|
||||
interface Props {
|
||||
list?: any[];
|
||||
|
|
@ -13,9 +15,10 @@ interface Props {
|
|||
isGraph?: boolean;
|
||||
zIndex?: number;
|
||||
noMargin?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
const EventRow = React.memo((props: Props) => {
|
||||
const { title, className, list = [], endTime = 0, isGraph = false, message = '' } = props;
|
||||
const { title, className, list = [], endTime = 0, isGraph = false, message = '', disabled } = props;
|
||||
const scale = 100 / endTime;
|
||||
const _list =
|
||||
isGraph ? [] :
|
||||
|
|
@ -82,7 +85,7 @@ const EventRow = React.memo((props: Props) => {
|
|||
}
|
||||
|
||||
return groupedItems;
|
||||
}, [list]);
|
||||
}, [list.length]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -91,21 +94,24 @@ const EventRow = React.memo((props: Props) => {
|
|||
>
|
||||
<div
|
||||
className={cn(
|
||||
'uppercase text-sm flex items-center py-1',
|
||||
'uppercase text-sm flex items-center py-1 gap-1',
|
||||
props.noMargin ? '' : 'ml-2'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
style={{ zIndex: props.zIndex ? props.zIndex : undefined }}
|
||||
className="mr-2 leading-none"
|
||||
className="leading-none mt-0.5"
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
{message ? <RowInfo message={message} /> : null}
|
||||
|
||||
<Tooltip title={message} placement='left'>
|
||||
<InfoCircleOutlined className='text-neutral-400' />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="relative w-full" style={{ zIndex: props.zIndex ? props.zIndex : undefined }}>
|
||||
{isGraph ? (
|
||||
<PerformanceGraph list={list} />
|
||||
<PerformanceGraph disabled={disabled} list={list} />
|
||||
) : _list.length > 0 ? (
|
||||
_list.map((item: { items: any[], left: number, isGrouped: boolean }, index: number) => {
|
||||
const left = item.left
|
||||
|
|
@ -123,7 +129,7 @@ const EventRow = React.memo((props: Props) => {
|
|||
);
|
||||
})
|
||||
) : (
|
||||
<div className={cn('color-gray-medium text-sm', props.noMargin ? '' : 'ml-4')}>
|
||||
<div className={cn('color-gray-medium text-xs', props.noMargin ? '' : 'ml-2')}>
|
||||
None captured.
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -133,11 +139,3 @@ const EventRow = React.memo((props: Props) => {
|
|||
});
|
||||
|
||||
export default EventRow;
|
||||
|
||||
function RowInfo({ message }: any) {
|
||||
return (
|
||||
<Tooltip title={message} delay={0}>
|
||||
<Icon name="info-circle" color="gray-medium" />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import React from 'react';
|
||||
import { Popover, Checkbox } from 'antd';
|
||||
import { Popover, Checkbox, Button } from 'antd';
|
||||
import {EyeInvisibleOutlined} from '@ant-design/icons';
|
||||
import { Icon } from 'UI'
|
||||
import Funnel from '@/types/funnel';
|
||||
|
||||
const NETWORK = 'NETWORK';
|
||||
const ERRORS = 'ERRORS';
|
||||
|
|
@ -59,7 +61,7 @@ function FeatureSelection(props: Props) {
|
|||
<Popover
|
||||
trigger="click"
|
||||
content={
|
||||
<div>
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div
|
||||
className={'flex items-center gap-2 cursor-pointer'}
|
||||
onClick={() => toggleAllFeatures()}
|
||||
|
|
@ -81,10 +83,9 @@ function FeatureSelection(props: Props) {
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<div className={'font-semibold flex items-center gap-2 text-main cursor-pointer'}>
|
||||
<Icon size={16} name={'funnel'} color={'main'} />
|
||||
<div>X-Ray Events</div>
|
||||
</div>
|
||||
<Button color='primary' size='small' type='text' className={'font-medium'} icon={<EyeInvisibleOutlined size={12} />} >
|
||||
Hide / Show
|
||||
</Button>
|
||||
</Popover>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,82 +1,106 @@
|
|||
import React from 'react';
|
||||
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
|
||||
import {InfoCircleOutlined} from '@ant-design/icons'
|
||||
|
||||
interface Props {
|
||||
list: any;
|
||||
list: any;
|
||||
disabled?: boolean;
|
||||
}
|
||||
const PerformanceGraph = React.memo((props: Props) => {
|
||||
const { list } = props;
|
||||
const { list, disabled } = props;
|
||||
|
||||
const finalValues = React.useMemo(() => {
|
||||
const cpuMax = list.reduce((acc: number, item: any) => {
|
||||
return Math.max(acc, item.cpu);
|
||||
}, 0);
|
||||
const cpuMin = list.reduce((acc: number, item: any) => {
|
||||
return Math.min(acc, item.cpu);
|
||||
}, Infinity);
|
||||
const finalValues = React.useMemo(() => {
|
||||
const cpuMax = list.reduce((acc: number, item: any) => {
|
||||
return Math.max(acc, item.cpu);
|
||||
}, 0);
|
||||
const cpuMin = list.reduce((acc: number, item: any) => {
|
||||
return Math.min(acc, item.cpu);
|
||||
}, Infinity);
|
||||
|
||||
const memoryMin = list.reduce((acc: number, item: any) => {
|
||||
return Math.min(acc, item.usedHeap);
|
||||
}, Infinity);
|
||||
const memoryMax = list.reduce((acc: number, item: any) => {
|
||||
return Math.max(acc, item.usedHeap);
|
||||
}, 0);
|
||||
const memoryMin = list.reduce((acc: number, item: any) => {
|
||||
return Math.min(acc, item.usedHeap);
|
||||
}, Infinity);
|
||||
const memoryMax = list.reduce((acc: number, item: any) => {
|
||||
return Math.max(acc, item.usedHeap);
|
||||
}, 0);
|
||||
|
||||
const convertToPercentage = (val: number, max: number, min: number) => {
|
||||
return ((val - min) / (max - min)) * 100;
|
||||
};
|
||||
const cpuValues = list.map((item: any) => convertToPercentage(item.cpu, cpuMax, cpuMin));
|
||||
const memoryValues = list.map((item: any) => convertToPercentage(item.usedHeap, memoryMax, memoryMin));
|
||||
const mergeArraysWithMaxNumber = (arr1: any[], arr2: any[]) => {
|
||||
const maxLength = Math.max(arr1.length, arr2.length);
|
||||
const result = [];
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const num = Math.round(Math.max(arr1[i] || 0, arr2[i] || 0));
|
||||
result.push(num > 60 ? num : 1);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const finalValues = mergeArraysWithMaxNumber(cpuValues, memoryValues);
|
||||
return finalValues;
|
||||
}, []);
|
||||
|
||||
const data = list.map((item: any, index: number) => {
|
||||
return {
|
||||
time: item.time,
|
||||
cpu: finalValues[index],
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<ResponsiveContainer height={35}>
|
||||
<AreaChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="cpuGradientTimeline" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="30%" stopColor="#CC0000" stopOpacity={0.5} />
|
||||
<stop offset="95%" stopColor="#3EAAAF" stopOpacity={0.8} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* <Tooltip filterNull={false} /> */}
|
||||
<Area
|
||||
dataKey="cpu"
|
||||
baseValue={5}
|
||||
type="monotone"
|
||||
stroke="none"
|
||||
activeDot={false}
|
||||
fill="url(#cpuGradientTimeline)"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
const convertToPercentage = (val: number, max: number, min: number) => {
|
||||
return ((val - min) / (max - min)) * 100;
|
||||
};
|
||||
const cpuValues = list.map((item: any) =>
|
||||
convertToPercentage(item.cpu, cpuMax, cpuMin)
|
||||
);
|
||||
const memoryValues = list.map((item: any) =>
|
||||
convertToPercentage(item.usedHeap, memoryMax, memoryMin)
|
||||
);
|
||||
const mergeArraysWithMaxNumber = (arr1: any[], arr2: any[]) => {
|
||||
const maxLength = Math.max(arr1.length, arr2.length);
|
||||
const result = [];
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const num = Math.round(Math.max(arr1[i] || 0, arr2[i] || 0));
|
||||
result.push(num > 60 ? num : 1);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const finalValues = mergeArraysWithMaxNumber(cpuValues, memoryValues);
|
||||
return finalValues;
|
||||
}, [list.length]);
|
||||
|
||||
const data = list.map((item: any, index: number) => {
|
||||
return {
|
||||
time: item.time,
|
||||
cpu: finalValues[index],
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={'relative'}>
|
||||
{disabled ? (
|
||||
<div
|
||||
className={
|
||||
'flex justify-start'
|
||||
}
|
||||
>
|
||||
<div className={'text-xs text-neutral-400 ps-2'}>
|
||||
Multi-tab performance overview is not available.</div>
|
||||
</div>
|
||||
) : null}
|
||||
<ResponsiveContainer height={35}>
|
||||
<AreaChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="cpuGradientTimeline"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="30%" stopColor="#CC0000" stopOpacity={0.5} />
|
||||
<stop offset="95%" stopColor="#3EAAAF" stopOpacity={0.8} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* <Tooltip filterNull={false} /> */}
|
||||
<Area
|
||||
dataKey="cpu"
|
||||
baseValue={5}
|
||||
type="monotone"
|
||||
stroke="none"
|
||||
activeDot={false}
|
||||
fill="url(#cpuGradientTimeline)"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default PerformanceGraph;
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export function FrustrationElement({ item, createEventClickHandler }: CommonProp
|
|||
const elData = getFrustration(item);
|
||||
return (
|
||||
<Tooltip
|
||||
placement={'right'}
|
||||
placement={'top'}
|
||||
title={
|
||||
<div className="">
|
||||
<b>{elData.name}</b>
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ function GroupedIssue({
|
|||
<div
|
||||
onClick={onClick}
|
||||
className={
|
||||
'h-5 w-5 cursor-pointer rounded-full bg-red text-white font-bold flex items-center justify-center text-sm'
|
||||
'h-5 w-5 cursor-pointer rounded-full bg-red text-white font-bold flex items-center justify-center text-xs'
|
||||
}
|
||||
>
|
||||
{items.length}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Loader, Icon } from 'UI';
|
||||
import { Loader } from 'UI';
|
||||
import {Button, Tooltip} from 'antd';
|
||||
import {CloseOutlined} from '@ant-design/icons';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import SelectorsList from './components/SelectorsList/SelectorsList';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { compareJsonObjects } from 'App/utils';
|
||||
|
||||
import Select from 'Shared/Select';
|
||||
import {Select, Form} from 'antd';
|
||||
|
||||
const JUMP_OFFSET = 1000;
|
||||
interface Props {
|
||||
|
|
@ -58,34 +60,29 @@ function PageInsightsPanel({ setActiveTab }: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white">
|
||||
<div className="pb-3 flex items-center" style={{ maxWidth: '241px', paddingTop: '5px' }}>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-1 text-xl">Clicks</span>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
setActiveTab('');
|
||||
}}
|
||||
className="ml-auto flex items-center justify-center bg-white cursor-pointer"
|
||||
>
|
||||
<Icon name="close" size="18" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center">
|
||||
<div className="mr-2 flex-shrink-0">In Page</div>
|
||||
<Select
|
||||
isSearchable={true}
|
||||
right
|
||||
placeholder="change"
|
||||
options={urlOptions}
|
||||
name="url"
|
||||
defaultValue={defaultValue}
|
||||
onChange={onPageSelect}
|
||||
id="change-dropdown"
|
||||
className="w-full"
|
||||
style={{ width: '100%' }}
|
||||
<div className="p-2 py-4 bg-white">
|
||||
<div className="flex items-center gap-2 mb-3 overflow-hidden">
|
||||
<div className="flex-shrink-0 font-medium">Page</div>
|
||||
<Form.Item name="url" className='mb-0 w-[176px]'>
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="change"
|
||||
options={urlOptions}
|
||||
defaultValue={defaultValue}
|
||||
onChange={onPageSelect}
|
||||
id="change-dropdown"
|
||||
className="w-full rounded-lg max-w-[270px]"
|
||||
dropdownStyle={{ }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title="Close Panel" placement='bottomRight'>
|
||||
<Button
|
||||
className="ml-2"
|
||||
type='text'
|
||||
onClick={() => { setActiveTab(''); }}
|
||||
icon={<CloseOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Loader loading={loading}>
|
||||
<SelectorsList />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
.wrapper {
|
||||
padding: 10px;
|
||||
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
padding: 1rem;
|
||||
background-color: $gray-lightest;
|
||||
margin-bottom: 15px;
|
||||
|
||||
|
|
@ -18,8 +16,6 @@
|
|||
border-radius: 10px;
|
||||
background-color: $tealx;
|
||||
flex-shrink: 0;
|
||||
border: solid thin white;
|
||||
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -17,20 +17,20 @@ export default function SelectorCard({ index = 1, target, showContent }: Props)
|
|||
|
||||
return (
|
||||
// @ts-ignore TODO for Alex
|
||||
<div className={cn(stl.wrapper, { [stl.active]: showContent })} onClick={() => activeTarget(index)}>
|
||||
<div className={cn(stl.wrapper, 'rounded-xl', { [stl.active]: showContent })} onClick={() => activeTarget(index)}>
|
||||
<div className={stl.top}>
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip position="top" title="Rank of the most clicked element">
|
||||
<div className={stl.index}>{index + 1}</div>
|
||||
</Tooltip>
|
||||
<div className="truncate">{target.selector}</div>
|
||||
<div className="truncate font-mono">{target.selector}</div>
|
||||
</div>
|
||||
{showContent && (
|
||||
<div className={stl.counts}>
|
||||
<div>
|
||||
{target.count} Clicks - {target.percent}%
|
||||
{target.count} Click{target.count > 1 ? 's' : ''} - {target.percent}%
|
||||
</div>
|
||||
<div className="color-gray-medium">TOTAL CLICKS</div>
|
||||
<div className="text-neutral-400">TOTAL CLICKS</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,12 +17,14 @@ import {
|
|||
} from 'recharts';
|
||||
import { durationFromMsFormatted } from 'App/date';
|
||||
import { formatBytes } from 'App/utils';
|
||||
import {Tooltip as TooltipANT} from 'antd';
|
||||
|
||||
import stl from './performance.module.css';
|
||||
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import InfoLine from '../BottomBlock/InfoLine';
|
||||
import { useStore } from 'App/mstore'
|
||||
import { Segmented } from 'antd'
|
||||
|
||||
const CPU_VISUAL_OFFSET = 10;
|
||||
|
||||
|
|
@ -457,15 +459,33 @@ function Performance() {
|
|||
return (
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="font-semibold color-gray-medium mr-auto">Performance</div>
|
||||
<InfoLine>
|
||||
<InfoLine.Point
|
||||
label="Device Heap Size"
|
||||
value={formatBytes(userDeviceHeapSize)}
|
||||
display={true}
|
||||
/>
|
||||
</InfoLine>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="font-semibold color-gray-medium mr-auto">Performance</div>
|
||||
<InfoLine>
|
||||
<InfoLine.Point
|
||||
label="Device Heap Size"
|
||||
value={formatBytes(userDeviceHeapSize)}
|
||||
display={true}
|
||||
/>
|
||||
</InfoLine>
|
||||
</div>
|
||||
|
||||
<div className={'flex items-center gap-3'}>
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: (
|
||||
<TooltipANT title="Performance overview isn't supported across tabs.">
|
||||
<span>All Tabs</span>
|
||||
</TooltipANT>
|
||||
), value: 'all', disabled: true, },
|
||||
{ label: 'Current Tab', value: 'current' },
|
||||
]}
|
||||
defaultValue="current"
|
||||
size="small"
|
||||
className="rounded-full font-medium"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { Icon } from 'UI';
|
|||
import LogsButton from 'App/components/Session/Player/SharedComponents/BackendLogs/LogsButton';
|
||||
|
||||
import ControlButton from './ControlButton';
|
||||
import { WebEventsList } from "./EventsList";
|
||||
import Timeline from './Timeline';
|
||||
import PlayerControls from './components/PlayerControls';
|
||||
import styles from './controls.module.css';
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import { PlayerContext, MobilePlayerContext } from 'Components/Session/playerCon
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { getTimelinePosition } from './getTimelinePosition'
|
||||
|
||||
function EventsList({ scale }: { scale: number }) {
|
||||
function EventsList() {
|
||||
const { store } = useContext(PlayerContext);
|
||||
|
||||
const { tabStates, eventCount } = store.get();
|
||||
const { eventCount, endTime } = store.get();
|
||||
const tabStates = store.get().tabStates;
|
||||
const scale = 100 / endTime;
|
||||
const events = React.useMemo(() => {
|
||||
return Object.values(tabStates)[0]?.eventList.filter(e => e.time) || [];
|
||||
}, [eventCount]);
|
||||
|
|
@ -34,11 +36,12 @@ function EventsList({ scale }: { scale: number }) {
|
|||
);
|
||||
}
|
||||
|
||||
function MobileEventsList({ scale }: { scale: number }) {
|
||||
function MobileEventsList() {
|
||||
const { store } = useContext(MobilePlayerContext);
|
||||
const { eventList } = store.get();
|
||||
const { eventList, endTime } = store.get();
|
||||
const events = eventList.filter(e => e.type !== 'SWIPE')
|
||||
|
||||
const scale = 100/endTime;
|
||||
return (
|
||||
<>
|
||||
{events.map((e) => (
|
||||
|
|
|
|||
|
|
@ -13,11 +13,7 @@ import NotesList from './NotesList';
|
|||
import SkipIntervalsList from './SkipIntervalsList';
|
||||
import TimelineTracker from 'Components/Session_/Player/Controls/TimelineTracker';
|
||||
|
||||
interface IProps {
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
function Timeline(props: IProps) {
|
||||
function Timeline({ isMobile }: { isMobile: boolean }) {
|
||||
const { player, store } = useContext(PlayerContext);
|
||||
const [wasPlaying, setWasPlaying] = useState(false);
|
||||
const [maxWidth, setMaxWidth] = useState(0);
|
||||
|
|
@ -126,6 +122,7 @@ function Timeline(props: IProps) {
|
|||
return Math.max(Math.round(p * targetTime), 0);
|
||||
};
|
||||
|
||||
console.log(devtoolsLoading , domLoading, !ready)
|
||||
return (
|
||||
<div
|
||||
className="flex items-center absolute w-full"
|
||||
|
|
@ -158,7 +155,7 @@ function Timeline(props: IProps) {
|
|||
{devtoolsLoading || domLoading || !ready ? <div className={stl.stripes} /> : null}
|
||||
</div>
|
||||
|
||||
{props.isMobile ? <MobEventsList scale={scale} /> : <WebEventsList scale={scale} />}
|
||||
{isMobile ? <MobEventsList /> : <WebEventsList />}
|
||||
<NotesList scale={scale} />
|
||||
<SkipIntervalsList scale={scale} />
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ function TimelineZoomButton() {
|
|||
}, [])
|
||||
return (
|
||||
<Tooltip title="Select a portion of the timeline to view the x-ray and activity for that specific selection." placement='top'>
|
||||
<Button onClick={onClickHandler} size={'small'} className={'flex items-center font-semibold'}>
|
||||
<Button onClick={onClickHandler} size={'small'} className={'flex items-center font-medium'}>
|
||||
Focus Mode: {enabled ? 'On' : 'Off'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
import React from 'react';
|
||||
import { useStore } from 'App/mstore'
|
||||
import { useStore } from 'App/mstore';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { JSONTree, NoContent, Tooltip } from 'UI';
|
||||
import { formatMs } from 'App/date';
|
||||
import diff from 'microdiff'
|
||||
import { STORAGE_TYPES, selectStorageList, selectStorageListNow, selectStorageType } from 'Player';
|
||||
import diff from 'microdiff';
|
||||
import {
|
||||
STORAGE_TYPES,
|
||||
selectStorageList,
|
||||
selectStorageListNow,
|
||||
selectStorageType,
|
||||
} from 'Player';
|
||||
import Autoscroll from '../Autoscroll';
|
||||
import BottomBlock from '../BottomBlock/index';
|
||||
import DiffRow from './DiffRow';
|
||||
import cn from 'classnames';
|
||||
import stl from './storage.module.css';
|
||||
import logger from "App/logger";
|
||||
import ReduxViewer from './ReduxViewer'
|
||||
import logger from 'App/logger';
|
||||
import ReduxViewer from './ReduxViewer';
|
||||
import { Segmented } from 'antd'
|
||||
|
||||
function getActionsName(type: string) {
|
||||
switch (type) {
|
||||
|
|
@ -31,7 +37,7 @@ const storageDecodeKeys = {
|
|||
[STORAGE_TYPES.ZUSTAND]: ['state', 'mutation'],
|
||||
[STORAGE_TYPES.MOBX]: ['payload'],
|
||||
[STORAGE_TYPES.NONE]: ['state, action', 'payload', 'mutation'],
|
||||
}
|
||||
};
|
||||
|
||||
function Storage() {
|
||||
const { uiPlayerStore } = useStore();
|
||||
|
|
@ -42,49 +48,48 @@ function Storage() {
|
|||
const [stateObject, setState] = React.useState({});
|
||||
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const { tabStates, currentTab } = store.get()
|
||||
const state = tabStates[currentTab] || {}
|
||||
const { tabStates, currentTab } = store.get();
|
||||
const state = tabStates[currentTab] || {};
|
||||
|
||||
const listNow = selectStorageListNow(state) || [];
|
||||
const list = selectStorageList(state) || [];
|
||||
const type = selectStorageType(state) || STORAGE_TYPES.NONE
|
||||
const type = selectStorageType(state) || STORAGE_TYPES.NONE;
|
||||
|
||||
React.useEffect(() => {
|
||||
let currentState;
|
||||
if (listNow.length === 0) {
|
||||
currentState = decodeMessage(list[0])
|
||||
currentState = decodeMessage(list[0]);
|
||||
} else {
|
||||
currentState = decodeMessage(listNow[listNow.length - 1])
|
||||
currentState = decodeMessage(listNow[listNow.length - 1]);
|
||||
}
|
||||
const stateObj = currentState?.state || currentState?.payload?.state || {}
|
||||
const stateObj = currentState?.state || currentState?.payload?.state || {};
|
||||
const newState = Object.assign(stateObject, stateObj);
|
||||
setState(newState);
|
||||
|
||||
}, [listNow.length]);
|
||||
|
||||
const decodeMessage = (msg: any) => {
|
||||
const decoded = {};
|
||||
const pureMSG = { ...msg }
|
||||
const pureMSG = { ...msg };
|
||||
const keys = storageDecodeKeys[type];
|
||||
try {
|
||||
keys.forEach(key => {
|
||||
keys.forEach((key) => {
|
||||
if (pureMSG[key]) {
|
||||
// @ts-ignore TODO: types for decoder
|
||||
decoded[key] = player.decodeMessage(pureMSG[key]);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("Error on message decoding: ", e, pureMSG);
|
||||
logger.error('Error on message decoding: ', e, pureMSG);
|
||||
return null;
|
||||
}
|
||||
return { ...pureMSG, ...decoded };
|
||||
}
|
||||
};
|
||||
|
||||
const decodedList = React.useMemo(() => {
|
||||
return listNow.map(msg => {
|
||||
return decodeMessage(msg)
|
||||
})
|
||||
}, [listNow.length])
|
||||
return listNow.map((msg) => {
|
||||
return decodeMessage(msg);
|
||||
});
|
||||
}, [listNow.length]);
|
||||
|
||||
const focusNextButton = () => {
|
||||
if (lastBtnRef.current) {
|
||||
|
|
@ -99,7 +104,10 @@ function Storage() {
|
|||
focusNextButton();
|
||||
}, [listNow]);
|
||||
|
||||
const renderDiff = (item: Record<string, any>, prevItem?: Record<string, any>) => {
|
||||
const renderDiff = (
|
||||
item: Record<string, any>,
|
||||
prevItem?: Record<string, any>
|
||||
) => {
|
||||
if (!showDiffs) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -113,7 +121,10 @@ function Storage() {
|
|||
|
||||
if (!stateDiff) {
|
||||
return (
|
||||
<div style={{ flex: 3 }} className="flex flex-col p-2 pr-0 font-mono text-disabled-text">
|
||||
<div
|
||||
style={{ flex: 3 }}
|
||||
className="flex flex-col p-2 pr-0 font-mono text-disabled-text"
|
||||
>
|
||||
No diff
|
||||
</div>
|
||||
);
|
||||
|
|
@ -121,13 +132,15 @@ function Storage() {
|
|||
|
||||
return (
|
||||
<div style={{ flex: 3 }} className="flex flex-col p-1 font-mono">
|
||||
{stateDiff.map((d: Record<string, any>, i: number) => renderDiffs(d, i))}
|
||||
{stateDiff.map((d: Record<string, any>, i: number) =>
|
||||
renderDiffs(d, i)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDiffs = (diff: Record<string, any>, i: number) => {
|
||||
const path = diff.path.join('.')
|
||||
const path = diff.path.join('.');
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
<DiffRow path={path} diff={diff} />
|
||||
|
|
@ -145,12 +158,16 @@ function Storage() {
|
|||
player.jump(list[listNow.length].time);
|
||||
};
|
||||
|
||||
const renderItem = (item: Record<string, any>, i: number, prevItem?: Record<string, any>) => {
|
||||
const renderItem = (
|
||||
item: Record<string, any>,
|
||||
i: number,
|
||||
prevItem?: Record<string, any>
|
||||
) => {
|
||||
let src;
|
||||
let name;
|
||||
|
||||
const itemD = item
|
||||
const prevItemD = prevItem ? prevItem : undefined
|
||||
const itemD = item;
|
||||
const prevItemD = prevItem ? prevItem : undefined;
|
||||
|
||||
switch (type) {
|
||||
case STORAGE_TYPES.REDUX:
|
||||
|
|
@ -177,7 +194,10 @@ function Storage() {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={cn('flex justify-between items-start', src !== null ? 'border-b' : '')}
|
||||
className={cn(
|
||||
'flex justify-between items-start',
|
||||
src !== null ? 'border-b' : ''
|
||||
)}
|
||||
key={`store-${i}`}
|
||||
>
|
||||
{src === null ? (
|
||||
|
|
@ -187,7 +207,10 @@ function Storage() {
|
|||
) : (
|
||||
<>
|
||||
{renderDiff(itemD, prevItemD)}
|
||||
<div style={{ flex: 2 }} className={cn("flex pt-2", showDiffs && 'pl-10')}>
|
||||
<div
|
||||
style={{ flex: 2 }}
|
||||
className={cn('flex pt-2', showDiffs && 'pl-10')}
|
||||
>
|
||||
<JSONTree
|
||||
name={ensureString(name)}
|
||||
src={src}
|
||||
|
|
@ -202,11 +225,16 @@ function Storage() {
|
|||
className="flex-1 flex gap-2 pt-2 items-center justify-end self-start"
|
||||
>
|
||||
{typeof item?.duration === 'number' && (
|
||||
<div className="font-size-12 color-gray-medium">{formatMs(itemD.duration)}</div>
|
||||
<div className="font-size-12 color-gray-medium">
|
||||
{formatMs(itemD.duration)}
|
||||
</div>
|
||||
)}
|
||||
<div className="w-12">
|
||||
{i + 1 < listNow.length && (
|
||||
<button className={stl.button} onClick={() => player.jump(item.time)}>
|
||||
<button
|
||||
className={stl.button}
|
||||
onClick={() => player.jump(item.time)}
|
||||
>
|
||||
{'JUMP'}
|
||||
</button>
|
||||
)}
|
||||
|
|
@ -222,31 +250,36 @@ function Storage() {
|
|||
};
|
||||
|
||||
if (type === STORAGE_TYPES.REDUX) {
|
||||
return <ReduxViewer />
|
||||
return <ReduxViewer />;
|
||||
}
|
||||
return (
|
||||
<BottomBlock>
|
||||
{/*@ts-ignore*/}
|
||||
<>
|
||||
<BottomBlock.Header>
|
||||
{list.length > 0 && (
|
||||
<div className="flex w-full">
|
||||
<h3 style={{ width: '25%', marginRight: 20 }} className="font-semibold">
|
||||
{'STATE'}
|
||||
</h3>
|
||||
{showDiffs ? (
|
||||
<h3 style={{ width: '39%' }} className="font-semibold">
|
||||
DIFFS
|
||||
</h3>
|
||||
) : null}
|
||||
<h3 style={{ width: '30%' }} className="font-semibold">
|
||||
{getActionsName(type)}
|
||||
</h3>
|
||||
<h3 style={{ paddingRight: 30, marginLeft: 'auto' }} className="font-semibold">
|
||||
<Tooltip title="Time to execute">TTE</Tooltip>
|
||||
</h3>
|
||||
<div className="flex w-full items-center">
|
||||
<div
|
||||
style={{ width: '25%', marginRight: 20 }}
|
||||
className="font-semibold flex items-center gap-2"
|
||||
>
|
||||
<h3>{'STATE'}</h3>
|
||||
</div>
|
||||
)}
|
||||
{showDiffs ? (
|
||||
<h3 style={{ width: '39%' }} className="font-semibold">
|
||||
DIFFS
|
||||
</h3>
|
||||
) : null}
|
||||
<h3 style={{ width: '30%' }} className="font-semibold">
|
||||
{getActionsName(type)}
|
||||
</h3>
|
||||
<h3
|
||||
style={{ paddingRight: 30, marginLeft: 'auto' }}
|
||||
className="font-semibold"
|
||||
>
|
||||
<Tooltip title="Time to execute">TTE</Tooltip>
|
||||
</h3>
|
||||
<Segmented options={[{ label: 'Current Tab', value: 'all' }]} />
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content className="flex">
|
||||
<NoContent
|
||||
|
|
@ -307,7 +340,10 @@ function Storage() {
|
|||
.
|
||||
<br />
|
||||
<br />
|
||||
<button className="color-teal" onClick={() => hideHint('storage')}>
|
||||
<button
|
||||
className="color-teal"
|
||||
onClick={() => hideHint('storage')}
|
||||
>
|
||||
Got It!
|
||||
</button>
|
||||
</>
|
||||
|
|
@ -322,8 +358,7 @@ function Storage() {
|
|||
{'Empty state.'}
|
||||
</div>
|
||||
) : (
|
||||
<JSONTree collapsed={2} src={stateObject}
|
||||
/>
|
||||
<JSONTree collapsed={2} src={stateObject} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex" style={{ width: '75%' }}>
|
||||
|
|
@ -342,7 +377,6 @@ function Storage() {
|
|||
|
||||
export default observer(Storage);
|
||||
|
||||
|
||||
/**
|
||||
* TODO: compute diff and only decode the required parts
|
||||
* WIP example
|
||||
|
|
@ -384,4 +418,4 @@ export default observer(Storage);
|
|||
* }, [list.length])
|
||||
* }
|
||||
*
|
||||
* */
|
||||
* */
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@ import cn from 'classnames';
|
|||
import cls from './infoLine.module.css';
|
||||
|
||||
const InfoLine = ({ children }) => (
|
||||
<div className={ cls.info }>
|
||||
<div className={ cn(cls.info, 'text-sm')}>
|
||||
{ children }
|
||||
</div>
|
||||
)
|
||||
|
||||
const Point = ({ label = '', value = '', display=true, color, dotColor }) => display
|
||||
? <div className={ cls.infoPoint } style={{ color }}>
|
||||
? <div className={ cn(cls.infoPoint, 'text-sm') } style={{ color }}>
|
||||
{ dotColor != null && <div className={ cn(cls.dot, `bg-${dotColor}`) } /> }
|
||||
<span className={cls.label}>{ `${label}` }</span> { value }
|
||||
<span className={cn(cls.label, 'text-sm')}>{ `${label}` }</span> { value }
|
||||
</div>
|
||||
: null;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
||||
import { LogLevel, ILog } from 'Player';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import { Tabs, Input, Icon, NoContent } from 'UI';
|
||||
import { Tabs, Icon, NoContent } from 'UI';
|
||||
import {Input} from 'antd';
|
||||
import {SearchOutlined, InfoCircleOutlined} from '@ant-design/icons';
|
||||
import cn from 'classnames';
|
||||
import ConsoleRow from '../ConsoleRow';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
|
|
@ -9,6 +11,7 @@ import { observer } from 'mobx-react-lite';
|
|||
import { useStore } from 'App/mstore';
|
||||
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import TabSelector from "../TabSelector";
|
||||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
||||
import { VList, VListHandle } from "virtua";
|
||||
|
|
@ -93,6 +96,7 @@ function ConsolePanel({
|
|||
sessionStore: { devTools },
|
||||
uiPlayerStore,
|
||||
} = useStore();
|
||||
|
||||
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
|
||||
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
|
||||
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
|
||||
|
|
@ -109,12 +113,22 @@ function ConsolePanel({
|
|||
const jump = (t: number) => player.jump(t);
|
||||
|
||||
const { currentTab, tabStates } = store.get();
|
||||
const {
|
||||
logList = [],
|
||||
exceptionsList = [],
|
||||
logListNow = [],
|
||||
exceptionsListNow = [],
|
||||
} = tabStates[currentTab] ?? {};
|
||||
const tabsArr = Object.keys(tabStates);
|
||||
const tabValues = Object.values(tabStates);
|
||||
const dataSource = uiPlayerStore.dataSource;
|
||||
const showSingleTab = dataSource === 'current';
|
||||
const { logList = [], exceptionsList = [], logListNow = [], exceptionsListNow = [] } = React.useMemo(() => {
|
||||
if (showSingleTab) {
|
||||
return tabStates[currentTab] ?? {};
|
||||
} else {
|
||||
const logList = tabValues.flatMap(tab => tab.logList);
|
||||
const exceptionsList = tabValues.flatMap(tab => tab.exceptionsList);
|
||||
const logListNow = isLive ? tabValues.flatMap(tab => tab.logListNow) : [];
|
||||
const exceptionsListNow = isLive ? tabValues.flatMap(tab => tab.exceptionsListNow) : [];
|
||||
return { logList, exceptionsList, logListNow, exceptionsListNow }
|
||||
}
|
||||
}, [currentTab, tabStates, dataSource, tabValues, isLive])
|
||||
const getTabNum = (tab: string) => (tabsArr.findIndex((t) => t === tab) + 1);
|
||||
|
||||
const list = isLive
|
||||
? (useMemo(
|
||||
|
|
@ -180,23 +194,26 @@ function ConsolePanel({
|
|||
<span className="font-semibold color-gray-medium mr-4">Console</span>
|
||||
<Tabs tabs={TABS} active={activeTab} onClick={onTabClick} border={false} />
|
||||
</div>
|
||||
<Input
|
||||
className="input-small h-8"
|
||||
placeholder="Filter by keyword"
|
||||
icon="search"
|
||||
name="filter"
|
||||
height={28}
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
/>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<TabSelector />
|
||||
<Input
|
||||
className="rounded-lg"
|
||||
placeholder="Filter by keyword"
|
||||
name="filter"
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
size='small'
|
||||
prefix={<SearchOutlined className='text-neutral-400' />}
|
||||
/>
|
||||
</div>
|
||||
{/* @ts-ignore */}
|
||||
</BottomBlock.Header>
|
||||
{/* @ts-ignore */}
|
||||
<BottomBlock.Content className="overflow-y-auto">
|
||||
<NoContent
|
||||
title={
|
||||
<div className="capitalize flex items-center mt-16">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
<div className="capitalize flex items-center mt-16 gap-2">
|
||||
<InfoCircleOutlined size={18} />
|
||||
No Data
|
||||
</div>
|
||||
}
|
||||
|
|
@ -211,6 +228,8 @@ function ConsolePanel({
|
|||
iconProps={getIconProps(log.level)}
|
||||
renderWithNL={renderWithNL}
|
||||
onClick={() => showDetails(log)}
|
||||
showSingleTab={showSingleTab}
|
||||
getTabNum={getTabNum}
|
||||
/>
|
||||
))}
|
||||
</VList>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorD
|
|||
import { useModal } from 'App/components/Modal';
|
||||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
||||
import {InfoCircleOutlined} from '@ant-design/icons'
|
||||
|
||||
const ALL = 'ALL';
|
||||
const INFO = 'INFO';
|
||||
|
|
@ -139,20 +140,20 @@ function MobileConsolePanel() {
|
|||
<Tabs tabs={TABS} active={activeTab} onClick={onTabClick} border={false} />
|
||||
</div>
|
||||
<Input
|
||||
className="input-small h-8"
|
||||
className="rounded-lg"
|
||||
placeholder="Filter by keyword"
|
||||
icon="search"
|
||||
name="filter"
|
||||
height={28}
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
size='small'
|
||||
prefix={<SearchOutlined className='text-neutral-400' />}
|
||||
/>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content className="overflow-y-auto">
|
||||
<NoContent
|
||||
title={
|
||||
<div className="capitalize flex items-center mt-16">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
<div className="capitalize flex items-center mt-16 gap-2">
|
||||
<InfoCircleOutlined size={18} />
|
||||
No Data
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import React, { useState } from 'react';
|
|||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import JumpButton from 'Shared/DevTools/JumpButton';
|
||||
import { Tag } from 'antd';
|
||||
import TabTag from "../TabTag";
|
||||
|
||||
interface Props {
|
||||
log: any;
|
||||
|
|
@ -10,6 +12,8 @@ interface Props {
|
|||
renderWithNL?: any;
|
||||
style?: any;
|
||||
onClick?: () => void;
|
||||
getTabNum: (tab: string) => number;
|
||||
showSingleTab: boolean;
|
||||
}
|
||||
function ConsoleRow(props: Props) {
|
||||
const { log, iconProps, jump, renderWithNL, style } = props;
|
||||
|
|
@ -41,11 +45,12 @@ function ConsoleRow(props: Props) {
|
|||
|
||||
const titleLine = lines[0];
|
||||
const restLines = lines.slice(1);
|
||||
const logSource = props.showSingleTab ? -1 : props.getTabNum(log.tabId);
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={cn(
|
||||
'border-b flex items-start py-1 px-4 pe-8 overflow-hidden group relative',
|
||||
'border-b border-neutral-950/5 flex items-start gap-2 py-1 px-4 pe-8 overflow-hidden group relative',
|
||||
{
|
||||
info: !log.isYellow && !log.isRed,
|
||||
warn: log.isYellow,
|
||||
|
|
@ -55,11 +60,10 @@ function ConsoleRow(props: Props) {
|
|||
)}
|
||||
onClick={clickable ? () => (!!log.errorId ? props.onClick?.() : toggleExpand()) : undefined}
|
||||
>
|
||||
<div className="mr-2">
|
||||
<Icon size="14" {...iconProps} />
|
||||
</div>
|
||||
{logSource !== -1 && <TabTag tabNum={logSource} />}
|
||||
<Icon size="14" {...iconProps} className='mt-0.5' />
|
||||
<div key={log.key} data-scroll-item={log.isRed}>
|
||||
<div className="flex items-start text-sm ">
|
||||
<div className="flex items-start text-sm">
|
||||
<div className={cn('flex items-start', { 'cursor-pointer underline decoration-dotted decoration-gray-400': !!log.errorId })}>
|
||||
{canExpand && (
|
||||
<Icon name={expanded ? 'caret-down-fill' : 'caret-right-fill'} className="mr-2" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import React from 'react';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import { shortDurationFromMs } from "App/date";
|
||||
import { Tooltip } from 'UI';
|
||||
import { CaretRightOutlined } from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
import { shortDurationFromMs } from 'App/date';
|
||||
|
||||
interface Props {
|
||||
onClick: any;
|
||||
|
|
@ -12,19 +14,24 @@ function JumpButton(props: Props) {
|
|||
return (
|
||||
<div className="absolute right-2 top-0 bottom-0 my-auto flex items-center">
|
||||
<Tooltip title={tooltip} disabled={!tooltip}>
|
||||
<div
|
||||
className="border cursor-pointer hidden group-hover:flex rounded bg-white text-xs items-center px-2 py-1 color-teal hover:shadow h-6"
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
className="hidden group-hover:flex rounded-lg text-xs p-1 py-0 gap-0 h-6"
|
||||
iconPosition="end"
|
||||
onClick={(e: any) => {
|
||||
e.stopPropagation();
|
||||
props.onClick();
|
||||
}}
|
||||
icon={<CaretRightOutlined />}
|
||||
>
|
||||
<Icon name="caret-right-fill" size="12" color="teal" />
|
||||
<span>JUMP</span>
|
||||
</div>
|
||||
{props.time ? <div className={'block group-hover:hidden mr-2'}>
|
||||
{shortDurationFromMs(props.time)}
|
||||
</div> : null}
|
||||
JUMP
|
||||
</Button>
|
||||
{props.time ? (
|
||||
<div className={'block group-hover:hidden mr-2 text-sm'}>
|
||||
{shortDurationFromMs(props.time)}
|
||||
</div>
|
||||
) : null}
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { ResourceType, Timed } from 'Player';
|
||||
import MobilePlayer from 'Player/mobile/IOSPlayer';
|
||||
import WebPlayer from 'Player/web/WebPlayer';
|
||||
import { Duration } from 'luxon';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
|
|
@ -13,17 +12,19 @@ import {
|
|||
import { formatMs } from 'App/date';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { formatBytes } from 'App/utils';
|
||||
import { Icon, Input, NoContent, Tabs, Toggler, Tooltip } from 'UI';
|
||||
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 { WsChannel } from "App/player/web/messages";
|
||||
import { WsChannel } from 'App/player/web/messages';
|
||||
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import InfoLine from '../BottomBlock/InfoLine';
|
||||
import TabSelector from '../TabSelector';
|
||||
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';
|
||||
|
|
@ -57,12 +58,6 @@ export const NETWORK_TABS = TAP_KEYS.map((tab) => ({
|
|||
const DOM_LOADED_TIME_COLOR = 'teal';
|
||||
const LOAD_TIME_COLOR = 'red';
|
||||
|
||||
function compare(a: any, b: any, key: string) {
|
||||
if (a[key] > b[key]) return 1;
|
||||
if (a[key] < b[key]) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function renderType(r: any) {
|
||||
return (
|
||||
<Tooltip style={{ width: '100%' }} title={<div>{r.type}</div>}>
|
||||
|
|
@ -79,14 +74,6 @@ export function renderName(r: any) {
|
|||
);
|
||||
}
|
||||
|
||||
export function renderStart(r: any) {
|
||||
return (
|
||||
<div className="flex justify-between items-center grow-0 w-full">
|
||||
<span>{Duration.fromMillis(r.time).toFormat('mm:ss.SSS')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSize(r: any) {
|
||||
if (r.responseBodySize) return formatBytes(r.responseBodySize);
|
||||
let triggerText;
|
||||
|
|
@ -125,13 +112,10 @@ export function renderDuration(r: any) {
|
|||
if (!r.isRed && !r.isYellow) return text;
|
||||
|
||||
let tooltipText;
|
||||
let className = 'w-full h-full flex items-center ';
|
||||
if (r.isYellow) {
|
||||
tooltipText = 'Slower than average';
|
||||
className += 'warn color-orange';
|
||||
} else {
|
||||
tooltipText = 'Much slower than average';
|
||||
className += 'error color-red';
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -151,7 +135,7 @@ function renderStatus({
|
|||
error?: string;
|
||||
}) {
|
||||
const displayedStatus = error ? (
|
||||
<Tooltip delay={0} title={error}>
|
||||
<Tooltip title={error}>
|
||||
<div
|
||||
style={{ width: 90 }}
|
||||
className={'overflow-hidden overflow-ellipsis'}
|
||||
|
|
@ -165,7 +149,7 @@ function renderStatus({
|
|||
return (
|
||||
<>
|
||||
{cached ? (
|
||||
<Tooltip title={'Served from cache'}>
|
||||
<Tooltip title={'Served from cache'} placement="top">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-1">{displayedStatus}</span>
|
||||
<Icon name="wifi" size={16} />
|
||||
|
|
@ -178,13 +162,10 @@ function renderStatus({
|
|||
);
|
||||
}
|
||||
|
||||
function NetworkPanelCont({
|
||||
panelHeight,
|
||||
}: {
|
||||
panelHeight: number;
|
||||
}) {
|
||||
function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const { sessionStore } = useStore();
|
||||
const { sessionStore, uiPlayerStore } = useStore();
|
||||
|
||||
const startedAt = sessionStore.current.startedAt;
|
||||
const {
|
||||
domContentLoadedTime,
|
||||
|
|
@ -193,6 +174,10 @@ function NetworkPanelCont({
|
|||
tabStates,
|
||||
currentTab,
|
||||
} = store.get();
|
||||
const tabsArr = Object.keys(tabStates);
|
||||
const tabValues = Object.values(tabStates);
|
||||
const dataSource = uiPlayerStore.dataSource;
|
||||
const showSingleTab = dataSource === 'current';
|
||||
const {
|
||||
fetchList = [],
|
||||
resourceList = [],
|
||||
|
|
@ -200,7 +185,33 @@ function NetworkPanelCont({
|
|||
resourceListNow = [],
|
||||
websocketList = [],
|
||||
websocketListNow = [],
|
||||
} = tabStates[currentTab];
|
||||
} = 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;
|
||||
|
||||
return (
|
||||
<NetworkPanelComp
|
||||
|
|
@ -216,15 +227,13 @@ function NetworkPanelCont({
|
|||
startedAt={startedAt}
|
||||
websocketList={websocketList as WSMessage[]}
|
||||
websocketListNow={websocketListNow as WSMessage[]}
|
||||
getTabNum={getTabNum}
|
||||
showSingleTab={showSingleTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileNetworkPanelCont({
|
||||
panelHeight,
|
||||
}: {
|
||||
panelHeight: number;
|
||||
}) {
|
||||
function MobileNetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
||||
const { player, store } = React.useContext(MobilePlayerContext);
|
||||
const { uiPlayerStore, sessionStore } = useStore();
|
||||
const startedAt = sessionStore.current.startedAt;
|
||||
|
|
@ -301,6 +310,8 @@ interface Props {
|
|||
onClose?: () => void;
|
||||
activeOutsideIndex?: number;
|
||||
isSpot?: boolean;
|
||||
getTabNum?: (tab: string) => number;
|
||||
showSingleTab?: boolean;
|
||||
}
|
||||
|
||||
export const NetworkPanelComp = observer(
|
||||
|
|
@ -323,8 +334,12 @@ export const NetworkPanelComp = observer(
|
|||
onClose,
|
||||
activeOutsideIndex,
|
||||
isSpot,
|
||||
getTabNum,
|
||||
showSingleTab,
|
||||
}: Props) => {
|
||||
const [selectedWsChannel, setSelectedWsChannel] = React.useState<WsChannel[] | null>(null)
|
||||
const [selectedWsChannel, setSelectedWsChannel] = React.useState<
|
||||
WsChannel[] | null
|
||||
>(null);
|
||||
const { showModal } = useModal();
|
||||
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
|
||||
|
||||
|
|
@ -480,10 +495,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 setSelectedWsChannel(socketMsgList)
|
||||
return setSelectedWsChannel(socketMsgList);
|
||||
}
|
||||
setIsDetailsModalActive(true);
|
||||
showModal(
|
||||
|
|
@ -507,6 +522,62 @@ export const NetworkPanelComp = observer(
|
|||
stopAutoscroll();
|
||||
};
|
||||
|
||||
const tableCols = React.useMemo(() => {
|
||||
const cols: any[] = [
|
||||
{
|
||||
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) {
|
||||
cols.unshift({
|
||||
label: 'Source',
|
||||
width: 64,
|
||||
render: (r: Record<string, any>) => (
|
||||
<Tooltip title="@Nikita show tab title here..." 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]);
|
||||
|
||||
return (
|
||||
<BottomBlock
|
||||
style={{ height: '100%' }}
|
||||
|
|
@ -529,26 +600,39 @@ export const NetworkPanelComp = observer(
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
className="input-small"
|
||||
placeholder="Filter by name, type, method or value"
|
||||
icon="search"
|
||||
name="filter"
|
||||
onChange={onFilterChange}
|
||||
height={28}
|
||||
width={280}
|
||||
value={filter}
|
||||
/>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<TabSelector />
|
||||
<Input
|
||||
className="rounded-lg"
|
||||
placeholder="Filter by name, type, method or value"
|
||||
name="filter"
|
||||
onChange={onFilterChange}
|
||||
width={280}
|
||||
value={filter}
|
||||
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>
|
||||
<Toggler
|
||||
checked={showOnlyErrors}
|
||||
name="show-errors-only"
|
||||
onChange={() => setShowOnlyErrors(!showOnlyErrors)}
|
||||
label="4xx-5xx Only"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<InfoLine>
|
||||
<InfoLine.Point
|
||||
|
|
@ -588,8 +672,8 @@ export const NetworkPanelComp = observer(
|
|||
</div>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="capitalize flex items-center">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
<div className="capitalize flex items-center gap-2">
|
||||
<InfoCircleOutlined size={18} />
|
||||
No Data
|
||||
</div>
|
||||
}
|
||||
|
|
@ -613,52 +697,13 @@ export const NetworkPanelComp = observer(
|
|||
}}
|
||||
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: '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,
|
||||
},
|
||||
]}
|
||||
{tableCols}
|
||||
</TimeTable>
|
||||
{selectedWsChannel ? (
|
||||
<WSPanel socketMsgList={selectedWsChannel} onClose={() => setSelectedWsChannel(null)} />
|
||||
<WSPanel
|
||||
socketMsgList={selectedWsChannel}
|
||||
onClose={() => setSelectedWsChannel(null)}
|
||||
/>
|
||||
) : null}
|
||||
</NoContent>
|
||||
</BottomBlock.Content>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { Timed } from 'Player';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Tabs, Input, NoContent, Icon } from 'UI';
|
||||
import { Tabs, NoContent, Icon } from 'UI';
|
||||
import {Input} from 'antd';
|
||||
import {SearchOutlined, InfoCircleOutlined} from '@ant-design/icons';
|
||||
import { PlayerContext, MobilePlayerContext } from 'App/components/Session/playerContext';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
|
|
@ -10,6 +12,7 @@ import { typeList } from 'Types/session/stackEvent';
|
|||
import StackEventRow from 'Shared/DevTools/StackEventRow';
|
||||
|
||||
import StackEventModal from '../StackEventModal';
|
||||
import { Segmented, Tooltip } from 'antd'
|
||||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
||||
import { VList, VListHandle } from 'virtua';
|
||||
|
|
@ -175,21 +178,37 @@ const EventsPanel = observer(({
|
|||
border={false}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
className="input-small h-8"
|
||||
placeholder="Filter by keyword"
|
||||
icon="search"
|
||||
name="filter"
|
||||
height={28}
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
/>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: 'All Tabs', value: 'all', },
|
||||
{ label: (
|
||||
<Tooltip title="Stack Events overview is available only for all tabs combined.">
|
||||
<span>Current Tab</span>
|
||||
</Tooltip>),
|
||||
value: 'current', disabled: true},
|
||||
]}
|
||||
defaultValue="all"
|
||||
size="small"
|
||||
className="rounded-full font-medium"
|
||||
/>
|
||||
<Input
|
||||
className="rounded-lg"
|
||||
placeholder="Filter by keyword"
|
||||
name="filter"
|
||||
height={28}
|
||||
onChange={onFilterChange}
|
||||
value={filter}
|
||||
size='small'
|
||||
prefix={<SearchOutlined className='text-neutral-400' />}
|
||||
/>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content className="overflow-y-auto">
|
||||
<NoContent
|
||||
title={
|
||||
<div className="capitalize flex items-center mt-16">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
<div className="capitalize flex items-center mt-16 gap-2">
|
||||
<InfoCircleOutlined size={18} />
|
||||
No Data
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
22
frontend/app/components/shared/DevTools/TabSelector.tsx
Normal file
22
frontend/app/components/shared/DevTools/TabSelector.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react'
|
||||
import { Segmented } from 'antd'
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
function TabSelector() {
|
||||
const { uiPlayerStore } = useStore();
|
||||
const currentValue = uiPlayerStore.dataSource;
|
||||
const options = [
|
||||
{ label: 'All Tabs', value: 'all' },
|
||||
{ label: 'Current Tab', value: 'current' }
|
||||
]
|
||||
|
||||
const onChange = (value: 'all' | 'current') => {
|
||||
uiPlayerStore.changeDataSource(value)
|
||||
}
|
||||
return (
|
||||
<Segmented options={options} value={currentValue} onChange={onChange} className='font-medium rounded-lg' size='small' />
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(TabSelector)
|
||||
15
frontend/app/components/shared/DevTools/TabTag.tsx
Normal file
15
frontend/app/components/shared/DevTools/TabTag.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
|
||||
function TabTag({ tabNum }: { tabNum?: React.ReactNode }) {
|
||||
return (
|
||||
|
||||
<Tooltip title="@Nikita show tab title here..." 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'}>
|
||||
{tabNum}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default TabTag
|
||||
|
|
@ -199,7 +199,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'dev-row border-b border-color-gray-light-shade group items-center',
|
||||
'dev-row border-b border-neutral-950/5 group items-center text-sm',
|
||||
stl.row,
|
||||
{
|
||||
[stl.hoverable]: hoverable,
|
||||
|
|
@ -215,7 +215,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
|
|||
{columns
|
||||
.filter((i: any) => !i.hidden)
|
||||
.map(({ dataKey, render, width, label }) => (
|
||||
<div key={parseInt(label.replace(' ', '')+dataKey, 36)} className={cn(stl.cell, 'overflow-ellipsis overflow-hidden')} style={{ width: `${width}px` }}>
|
||||
<div key={parseInt(label.replace(' ', '')+dataKey, 36)} className={cn(stl.cell, 'overflow-ellipsis overflow-hidden !py-0.5')} style={{ width: `${width}px` }}>
|
||||
{render
|
||||
? render(row)
|
||||
: row[dataKey || ''] || <i className="color-gray-light">{'empty'}</i>}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,11 @@
|
|||
import React from 'react';
|
||||
// import Select from 'Shared/Select';
|
||||
import { Dropdown, MenuProps, Select, Space } from 'antd';
|
||||
import { DownOutlined, SmileOutlined } from '@ant-design/icons';
|
||||
import { MenuProps, Select } from 'antd';
|
||||
|
||||
interface Props {
|
||||
payload: any;
|
||||
}
|
||||
|
||||
function NodeDropdown(props: Props) {
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<a target='_blank' rel='noopener noreferrer' href='https://www.antgroup.com'>
|
||||
1st menu item
|
||||
</a>
|
||||
)
|
||||
}
|
||||
];
|
||||
return (
|
||||
<Select style={{ width: 120 }} placeholder='Slect Event' dropdownStyle={{
|
||||
border: 'none'
|
||||
|
|
|
|||
|
|
@ -66,11 +66,16 @@ export default class UiPlayerStore {
|
|||
endTs: 0,
|
||||
}
|
||||
zoomTab: 'overview' | 'journey' | 'issues' | 'errors' = 'overview'
|
||||
dataSource: 'all' | 'current' = 'all'
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
changeDataSource = (source: 'all' | 'current') => {
|
||||
this.dataSource = source;
|
||||
}
|
||||
|
||||
toggleFullscreen = (val?: boolean) => {
|
||||
this.fullscreen = val ?? !this.fullscreen;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
import { Store } from './types'
|
||||
|
||||
export default class SimpleSore<G extends Object, S extends Object = G> implements Store<G, S> {
|
||||
export default class SimpleStore<G extends Record<string, any>, S extends Record<string, any> = G> implements Store<G, S> {
|
||||
constructor(private state: G){}
|
||||
get(): G {
|
||||
return this.state
|
||||
}
|
||||
update(newState: Partial<S>) {
|
||||
update = (newState: Partial<S>) => {
|
||||
Object.assign(this.state, newState)
|
||||
}
|
||||
updateTabStates = (id: string, newState: Partial<S>) => {
|
||||
try {
|
||||
Object.assign(this.state.tabStates[id], newState)
|
||||
} catch (e) {
|
||||
console.log('Error updating tab state', e, id, newState, this.state, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export interface Interval {
|
|||
export interface Store<G extends Object, S extends Object = G> {
|
||||
get(): G
|
||||
update(state: Partial<S>): void
|
||||
updateTabStates(id: string, state: Partial<S>): void
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@ export default class MessageLoader {
|
|||
try {
|
||||
await this.loadMobs();
|
||||
} catch (sessionLoadError) {
|
||||
console.info('!', sessionLoadError);
|
||||
try {
|
||||
await this.loadEFSMobs();
|
||||
} catch (unprocessedLoadError) {
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export default class MessageManager {
|
|||
closedTabs: [],
|
||||
sessionStart: 0,
|
||||
tabNames: {},
|
||||
};
|
||||
};
|
||||
|
||||
private clickManager: ListWalker<MouseClick> = new ListWalker();
|
||||
private mouseThrashingManager: ListWalker<MouseThrashing> = new ListWalker();
|
||||
|
|
@ -179,6 +179,7 @@ export default class MessageManager {
|
|||
this.activityManager.end();
|
||||
this.state.update({ skipIntervals: this.activityManager.list });
|
||||
}
|
||||
|
||||
Object.values(this.tabs).forEach((tab) => tab.onFileReadSuccess?.());
|
||||
};
|
||||
|
||||
|
|
@ -317,6 +318,7 @@ export default class MessageManager {
|
|||
if (msg.tp === 9999) return;
|
||||
if (!this.tabs[msg.tabId]) {
|
||||
this.tabsAmount++;
|
||||
this.state.update({ tabStates: { ...this.state.get().tabStates, [msg.tabId]: TabSessionManager.INITIAL_STATE } });
|
||||
this.tabs[msg.tabId] = new TabSessionManager(
|
||||
this.session,
|
||||
this.state,
|
||||
|
|
|
|||
|
|
@ -163,15 +163,7 @@ export default class TabSessionManager {
|
|||
* Because we use main state (from messageManager), we have to update it this way
|
||||
* */
|
||||
updateLocalState(state: Partial<TabState>) {
|
||||
this.state.update({
|
||||
tabStates: {
|
||||
...this.state.get().tabStates,
|
||||
[this.id]: {
|
||||
...this.state.get().tabStates[this.id],
|
||||
...state,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.state.updateTabStates(this.id, state);
|
||||
}
|
||||
|
||||
private setCSSLoading = (cssLoading: boolean) => {
|
||||
|
|
@ -414,8 +406,9 @@ export default class TabSessionManager {
|
|||
}
|
||||
|
||||
Object.assign(stateToUpdate, this.lists.moveGetState(t));
|
||||
Object.keys(stateToUpdate).length > 0 &&
|
||||
if (Object.keys(stateToUpdate).length > 0) {
|
||||
this.updateLocalState(stateToUpdate);
|
||||
}
|
||||
/* Sequence of the managers is important here */
|
||||
// Preparing the size of "screen"
|
||||
const lastResize = this.resizeManager.moveGetLast(t, index);
|
||||
|
|
|
|||
|
|
@ -436,3 +436,7 @@ p {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ant-segmented-item{
|
||||
border-radius: .5rem !important;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue