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:
Delirium 2024-12-10 10:31:09 +01:00 committed by GitHub
parent c963ec5e91
commit 38594319f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 814 additions and 483 deletions

View file

@ -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',

View file

@ -1,6 +1,5 @@
import { useStore } from 'App/mstore';
import React from 'react';
// import Select from 'Shared/Select';
import { Select } from 'antd';
const sortOptions = [

View file

@ -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 = {

View file

@ -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}

View file

@ -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">

View file

@ -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>
);
}

View file

@ -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);

View file

@ -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>
);

View file

@ -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>

View file

@ -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>

View file

@ -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>
)}

View file

@ -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>

View file

@ -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)}

View file

@ -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

View file

@ -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>
);
}

View file

@ -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>
);

View file

@ -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;

View file

@ -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>

View file

@ -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}

View file

@ -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 />

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -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';

View file

@ -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) => (

View file

@ -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} />

View file

@ -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>

View file

@ -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])
* }
*
* */
* */

View file

@ -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;

View file

@ -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>

View file

@ -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>
}

View file

@ -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" />

View file

@ -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>
);

View file

@ -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>

View file

@ -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>
}

View 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)

View 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

View file

@ -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>}

View file

@ -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'

View file

@ -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;
}

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -236,6 +236,7 @@ export default class MessageLoader {
try {
await this.loadMobs();
} catch (sessionLoadError) {
console.info('!', sessionLoadError);
try {
await this.loadEFSMobs();
} catch (unprocessedLoadError) {

View file

@ -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,

View file

@ -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);

View file

@ -436,3 +436,7 @@ p {
display: flex;
align-items: center;
}
.ant-segmented-item{
border-radius: .5rem !important;
}