Merge pull request #833 from openreplay/dev-tools-sync

dev tools sync - Network, Console, Events
This commit is contained in:
Shekar Siri 2022-11-24 10:57:06 +01:00 committed by GitHub
commit c773ed99ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 639 additions and 257 deletions

View file

@ -42,6 +42,7 @@ import { updateLastPlayedSession } from 'Duck/sessions';
import OverviewPanel from '../OverviewPanel';
import ConsolePanel from 'Shared/DevTools/ConsolePanel';
import ProfilerPanel from 'Shared/DevTools/ProfilerPanel';
import StackEventPanel from 'Shared/DevTools/StackEventPanel';
@connectPlayer((state) => ({
live: state.live,
@ -115,7 +116,8 @@ export default class Player extends React.PureComponent {
// <Network />
<NetworkPanel />
)}
{bottomBlock === STACKEVENTS && <StackEvents />}
{/* {bottomBlock === STACKEVENTS && <StackEvents />} */}
{bottomBlock === STACKEVENTS && <StackEventPanel />}
{bottomBlock === STORAGE && <Storage />}
{bottomBlock === PROFILER && <ProfilerPanel />}
{bottomBlock === PERFORMANCE && <ConnectedPerformance />}

View file

@ -1,17 +1,29 @@
import React from 'react';
import React, { useEffect } from 'react';
import cn from 'classnames';
import stl from './bottomBlock.module.css';
let timer = null;
const BottomBlock = ({
children = null,
className = '',
additionalHeight = 0,
onMouseEnter = () => {},
onMouseLeave = () => {},
...props
}) => (
<div className={ cn(stl.wrapper, "flex flex-col mb-2") } { ...props } >
{ children }
</div>
);
}) => {
useEffect(() => {}, []);
return (
<div
className={cn(stl.wrapper, 'flex flex-col mb-2')}
{...props}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{children}
</div>
);
};
BottomBlock.displayName = 'BottomBlock';

View file

@ -1,19 +1,17 @@
import React, { useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { connectPlayer, jump } from 'Player';
import Log from 'Types/session/log';
import BottomBlock from '../BottomBlock';
import { LEVEL } from 'Types/session/log';
import { Tabs, Input, Icon, NoContent } from 'UI';
// import Autoscroll from 'App/components/Session_/Autoscroll';
import cn from 'classnames';
import ConsoleRow from '../ConsoleRow';
import { getRE } from 'App/utils';
import {
List,
CellMeasurer,
CellMeasurerCache,
AutoSizer,
} from 'react-virtualized';
import { List, CellMeasurer, CellMeasurerCache, AutoSizer } from 'react-virtualized';
import { useObserver } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
import { useModal } from 'App/components/Modal';
const ALL = 'ALL';
const INFO = 'INFO';
@ -58,26 +56,97 @@ const getIconProps = (level: any) => {
return null;
};
const INDEX_KEY = 'console';
let timeOut: any = null;
const TIMEOUT_DURATION = 5000;
interface Props {
logs: any;
exceptions: any;
time: any;
}
function ConsolePanel(props: Props) {
const { logs } = props;
const { logs, time } = props;
const additionalHeight = 0;
const [activeTab, setActiveTab] = useState(ALL);
const [filter, setFilter] = useState('');
// const [activeTab, setActiveTab] = useState(ALL);
// const [filter, setFilter] = useState('');
const {
sessionStore: { devTools },
} = useStore();
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
const [filteredList, setFilteredList] = useState([]);
const filter = useObserver(() => devTools[INDEX_KEY].filter);
const activeTab = useObserver(() => devTools[INDEX_KEY].activeTab);
const activeIndex = useObserver(() => devTools[INDEX_KEY].index);
const [pauseSync, setPauseSync] = useState(activeIndex > 0);
const synRef: any = useRef({});
const { showModal } = useModal();
const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab });
const onFilterChange = ({ target: { value } }: any) => {
devTools.update(INDEX_KEY, { filter: value });
};
synRef.current = {
pauseSync,
activeIndex,
};
const removePause = () => {
setIsDetailsModalActive(false);
clearTimeout(timeOut);
timeOut = setTimeout(() => {
devTools.update(INDEX_KEY, { index: getCurrentIndex() });
setPauseSync(false);
}, TIMEOUT_DURATION);
};
const onMouseLeave = () => {
if (isDetailsModalActive) return;
removePause();
};
useEffect(() => {
if (pauseSync) {
removePause();
}
return () => {
clearTimeout(timeOut);
if (!synRef.current.pauseSync) {
devTools.update(INDEX_KEY, { index: 0 });
}
};
}, []);
const getCurrentIndex = () => {
return filteredList.filter((item: any) => item.time <= time).length - 1;
};
useEffect(() => {
const currentIndex = getCurrentIndex();
if (currentIndex !== activeIndex && !pauseSync) {
devTools.update(INDEX_KEY, { index: currentIndex });
}
}, [time]);
const cache = new CellMeasurerCache({
fixedWidth: true,
keyMapper: (index: number) => filtered[index],
keyMapper: (index: number) => filteredList[index],
});
const _list = React.useRef();
const showDetails = (log: any) => {
setIsDetailsModalActive(true);
showModal(<ErrorDetailsModal errorId={log.errorId} />, { right: true, onClose: removePause });
devTools.update(INDEX_KEY, { index: filteredList.indexOf(log) });
setPauseSync(true);
};
const _rowRenderer = ({ index, key, parent, style }: any) => {
const item = filtered[index];
const item = filteredList[index];
return (
// @ts-ignore
<CellMeasurer cache={cache} columnIndex={0} key={key} rowIndex={index} parent={parent}>
{({ measure }: any) => (
<ConsoleRow
@ -86,6 +155,7 @@ function ConsolePanel(props: Props) {
jump={jump}
iconProps={getIconProps(item.level)}
renderWithNL={renderWithNL}
onClick={() => showDetails(item)}
recalcHeight={() => {
measure();
(_list as any).current.recomputeRowHeights(index);
@ -96,7 +166,7 @@ function ConsolePanel(props: Props) {
);
};
let filtered = React.useMemo(() => {
React.useMemo(() => {
const filterRE = getRE(filter, 'i');
let list = logs;
@ -105,14 +175,23 @@ function ConsolePanel(props: Props) {
(!!filter ? filterRE.test(value) : true) &&
(activeTab === ALL || activeTab === LEVEL_TAB[level])
);
return list;
}, [filter, activeTab]);
setFilteredList(list);
}, [logs, filter, activeTab]);
const onTabClick = (activeTab: any) => setActiveTab(activeTab);
const onFilterChange = ({ target: { value } }: any) => setFilter(value);
useEffect(() => {
if (_list.current) {
// @ts-ignore
_list.current.scrollToRow(activeIndex);
}
}, [activeIndex]);
return (
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }}>
<BottomBlock
style={{ height: 300 + additionalHeight + 'px' }}
onMouseEnter={() => setPauseSync(true)}
onMouseLeave={onMouseLeave}
>
{/* @ts-ignore */}
<BottomBlock.Header>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Console</span>
@ -126,8 +205,11 @@ function ConsolePanel(props: Props) {
name="filter"
height={28}
onChange={onFilterChange}
value={filter}
/>
{/* @ts-ignore */}
</BottomBlock.Header>
{/* @ts-ignore */}
<BottomBlock.Content className="overflow-y-auto">
<NoContent
title={
@ -137,23 +219,28 @@ function ConsolePanel(props: Props) {
</div>
}
size="small"
show={filtered.length === 0}
show={filteredList.length === 0}
>
{/* @ts-ignore */}
<AutoSizer>
{({ height, width }: any) => (
// @ts-ignore
<List
ref={_list}
deferredMeasurementCache={cache}
overscanRowCount={5}
rowCount={Math.ceil(filtered.length || 1)}
rowCount={Math.ceil(filteredList.length || 1)}
rowHeight={cache.rowHeight}
rowRenderer={_rowRenderer}
width={width}
height={height}
// scrollToIndex={activeIndex}
scrollToAlignment="center"
/>
)}
</AutoSizer>
</NoContent>
{/* @ts-ignore */}
</BottomBlock.Content>
</BottomBlock>
);
@ -171,6 +258,7 @@ export default connectPlayer((state: any) => {
})
);
return {
time: state.time,
logs: logs.concat(logExceptions),
};
})(ConsolePanel);

View file

@ -2,8 +2,6 @@ import React, { useState } from 'react';
import cn from 'classnames';
import { Icon } from 'UI';
import JumpButton from 'Shared/DevTools/JumpButton';
import { useModal } from 'App/components/Modal';
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
interface Props {
log: any;
@ -12,24 +10,20 @@ interface Props {
renderWithNL?: any;
style?: any;
recalcHeight?: () => void;
onClick: () => void;
}
function ConsoleRow(props: Props) {
const { log, iconProps, jump, renderWithNL, style, recalcHeight } = props;
const { showModal } = useModal();
const [expanded, setExpanded] = useState(false);
const lines = log.value.split('\n').filter((l: any) => !!l);
const canExpand = lines.length > 1;
const clickable = canExpand || !!log.errorId;
const onErrorClick = () => {
showModal(<ErrorDetailsModal errorId={log.errorId} />, { right: true });
};
const toggleExpand = () => {
setExpanded(!expanded)
setTimeout(() => recalcHeight(), 0)
}
setExpanded(!expanded);
setTimeout(() => recalcHeight(), 0);
};
return (
<div
style={style}
@ -43,9 +37,7 @@ function ConsoleRow(props: Props) {
'cursor-pointer underline decoration-dotted decoration-gray-200': !!log.errorId,
}
)}
onClick={
clickable ? () => (!!log.errorId ? onErrorClick() : toggleExpand()) : () => {}
}
onClick={clickable ? () => (!!log.errorId ? props.onClick() : toggleExpand()) : () => {}}
>
<div className="mr-2">
<Icon size="14" {...iconProps} />
@ -57,7 +49,13 @@ function ConsoleRow(props: Props) {
)}
<span>{renderWithNL(lines.pop())}</span>
</div>
{canExpand && expanded && lines.map((l: string, i: number) => <div key={l.slice(0,4)+i} className="ml-4 mb-1">{l}</div>)}
{canExpand &&
expanded &&
lines.map((l: string, i: number) => (
<div key={l.slice(0, 4) + i} className="ml-4 mb-1">
{l}
</div>
))}
</div>
<JumpButton onClick={() => jump(log.time)} />
</div>

View file

@ -6,10 +6,10 @@ interface Props {
tooltip?: string;
}
function JumpButton(props: Props) {
const { tooltip = '' } = props;
const { tooltip } = props;
return (
<div className="absolute right-0 top-0 bottom-0 my-auto flex items-center">
<Tooltip title={tooltip} disabled={!!tooltip}>
<Tooltip title={tooltip} disabled={!tooltip}>
<div
className="mr-2 border cursor-pointer invisible group-hover:visible rounded-lg bg-active-blue text-xs flex items-center px-2 py-1 color-teal hover:shadow h-6"
onClick={(e: any) => {

View file

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { QuestionMarkHint, Tooltip, Tabs, Input, NoContent, Icon, Toggler, Button } from 'UI';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Tooltip, Tabs, Input, NoContent, Icon, Toggler } from 'UI';
import { getRE } from 'App/utils';
import Resource, { TYPES } from 'Types/session/resource';
import { formatBytes } from 'App/utils';
@ -12,6 +12,10 @@ import { Duration } from 'luxon';
import { connectPlayer, jump } from 'Player';
import { useModal } from 'App/components/Modal';
import FetchDetailsModal from 'Shared/FetchDetailsModal';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
const INDEX_KEY = 'network';
const ALL = 'ALL';
const XHR = 'xhr';
@ -67,37 +71,6 @@ export function renderStart(r: any) {
);
}
// const renderXHRText = () => (
// <span className="flex items-center">
// {XHR}
// <QuestionMarkHint
// content={
// <>
// Use our{' '}
// <a
// className="color-teal underline"
// target="_blank"
// href="https://docs.openreplay.com/plugins/fetch"
// >
// Fetch plugin
// </a>
// {' to capture HTTP requests and responses, including status codes and bodies.'} <br />
// We also provide{' '}
// <a
// className="color-teal underline"
// target="_blank"
// href="https://docs.openreplay.com/plugins/graphql"
// >
// support for GraphQL
// </a>
// {' for easy debugging of your queries.'}
// </>
// }
// className="ml-1"
// />
// </span>
// );
function renderSize(r: any) {
if (r.responseBodySize) return formatBytes(r.responseBodySize);
let triggerText;
@ -152,6 +125,9 @@ export function renderDuration(r: any) {
);
}
let timeOut: any = null;
const TIMEOUT_DURATION = 5000;
interface Props {
location: any;
resources: any;
@ -160,58 +136,101 @@ interface Props {
loadTime: any;
playing: boolean;
domBuildingTime: any;
currentIndex: any;
time: any;
}
function NetworkPanel(props: Props) {
const {
resources,
time,
currentIndex,
domContentLoadedTime,
loadTime,
playing,
domBuildingTime,
fetchList,
} = props;
const { showModal, hideModal } = useModal();
const [activeTab, setActiveTab] = useState(ALL);
const [sortBy, setSortBy] = useState('time');
const [sortAscending, setSortAscending] = useState(true);
const [filter, setFilter] = useState('');
const { resources, time, domContentLoadedTime, loadTime, domBuildingTime, fetchList } = props;
const { showModal } = useModal();
const [filteredList, setFilteredList] = useState([]);
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
const [activeRequest, setActiveRequest] = useState(false )
const onTabClick = (activeTab: any) => setActiveTab(activeTab);
const onFilterChange = ({ target: { value } }: any) => setFilter(value);
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
const additionalHeight = 0;
const fetchPresented = fetchList.length > 0;
const {
sessionStore: { devTools },
} = useStore();
// const [filter, setFilter] = useState(devTools[INDEX_KEY].filter);
// const [activeTab, setActiveTab] = useState(ALL);
const filter = useObserver(() => devTools[INDEX_KEY].filter);
const activeTab = useObserver(() => devTools[INDEX_KEY].activeTab);
const activeIndex = useObserver(() => devTools[INDEX_KEY].index);
const [pauseSync, setPauseSync] = useState(activeIndex > 0);
const synRef: any = useRef({});
const resourcesSize = resources.reduce(
(sum: any, { decodedBodySize }: any) => sum + (decodedBodySize || 0),
0
);
const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab });
const onFilterChange = ({ target: { value } }: any) => {
devTools.update(INDEX_KEY, { filter: value });
};
const transferredSize = resources.reduce(
(sum: any, { headerSize, encodedBodySize }: any) =>
sum + (headerSize || 0) + (encodedBodySize || 0),
0
);
synRef.current = {
pauseSync,
activeIndex,
};
const filterRE = getRE(filter, 'i');
let filtered = React.useMemo(() => {
const removePause = () => {
setIsDetailsModalActive(false);
clearTimeout(timeOut);
timeOut = setTimeout(() => {
devTools.update(INDEX_KEY, { index: getCurrentIndex() });
setPauseSync(false);
}, TIMEOUT_DURATION);
};
const onMouseLeave = () => {
if (isDetailsModalActive) return;
removePause();
};
useEffect(() => {
if (pauseSync) {
removePause();
}
return () => {
clearTimeout(timeOut);
if (!synRef.current.pauseSync) {
devTools.update(INDEX_KEY, { index: 0 });
}
};
}, []);
const getCurrentIndex = () => {
return filteredList.filter((item: any) => item.time <= time).length - 1;
};
useEffect(() => {
const currentIndex = getCurrentIndex();
if (currentIndex !== activeIndex && !pauseSync) {
devTools.update(INDEX_KEY, { index: currentIndex });
}
}, [time]);
const { resourcesSize, transferredSize } = useMemo(() => {
const resourcesSize = resources.reduce(
(sum: any, { decodedBodySize }: any) => sum + (decodedBodySize || 0),
0
);
const transferredSize = resources.reduce(
(sum: any, { headerSize, encodedBodySize }: any) =>
sum + (headerSize || 0) + (encodedBodySize || 0),
0
);
return {
resourcesSize,
transferredSize,
};
}, [resources]);
useEffect(() => {
const filterRE = getRE(filter, 'i');
let list = resources;
fetchList.forEach(
(fetchCall: any) =>
(list = list.filter((networkCall: any) => networkCall.url !== fetchCall.url))
);
list = list.concat(fetchList);
list = list.sort((a: any, b: any) => {
return compare(a, b, sortBy);
});
if (!sortAscending) {
list = list.reverse();
}
list = list.filter(
({ type, name, status, success }: any) =>
@ -219,41 +238,53 @@ function NetworkPanel(props: Props) {
(activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab]) &&
(showOnlyErrors ? parseInt(status) >= 400 || !success : true)
);
return list;
}, [filter, sortBy, sortAscending, showOnlyErrors, activeTab]);
setFilteredList(list);
}, [resources, filter, showOnlyErrors, activeTab]);
// const lastIndex = currentIndex || filtered.filter((item: any) => item.time <= time).length - 1;
const referenceLines = [];
if (domContentLoadedTime != null) {
referenceLines.push({
time: domContentLoadedTime.time,
color: DOM_LOADED_TIME_COLOR,
});
}
if (loadTime != null) {
referenceLines.push({
time: loadTime.time,
color: LOAD_TIME_COLOR,
});
}
const referenceLines = useMemo(() => {
const arr = [];
const onRowClick = (row: any) => {
showModal(<FetchDetailsModal resource={row} rows={filtered} fetchPresented={fetchPresented} />, {
right: true,
});
};
const handleSort = (sortKey: string) => {
if (sortKey === sortBy) {
setSortAscending(!sortAscending);
// setSortBy('time');
if (domContentLoadedTime != null) {
arr.push({
time: domContentLoadedTime.time,
color: DOM_LOADED_TIME_COLOR,
});
}
setSortBy(sortKey);
if (loadTime != null) {
arr.push({
time: loadTime.time,
color: LOAD_TIME_COLOR,
});
}
return arr;
}, []);
const showDetailsModal = (row: any) => {
setIsDetailsModalActive(true);
showModal(
<FetchDetailsModal resource={row} rows={filteredList} fetchPresented={fetchPresented} />,
{
right: true,
onClose: removePause,
}
);
devTools.update(INDEX_KEY, { index: filteredList.indexOf(row) });
setPauseSync(true);
};
useEffect(() => {
devTools.update(INDEX_KEY, { filter, activeTab });
}, [filter, activeTab]);
return (
<React.Fragment>
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }} className="border">
<BottomBlock
style={{ height: 300 + additionalHeight + 'px' }}
className="border"
onMouseEnter={() => setPauseSync(true)}
onMouseLeave={onMouseLeave}
>
<BottomBlock.Header>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Network</span>
@ -274,6 +305,7 @@ function NetworkPanel(props: Props) {
onChange={onFilterChange}
height={28}
width={230}
value={filter}
/>
</BottomBlock.Header>
<BottomBlock.Content>
@ -287,7 +319,7 @@ function NetworkPanel(props: Props) {
/>
</div>
<InfoLine>
<InfoLine.Point label={filtered.length} value=" requests" />
<InfoLine.Point label={filteredList.length + ''} value=" requests" />
<InfoLine.Point
label={formatBytes(transferredSize)}
value="transferred"
@ -325,18 +357,20 @@ function NetworkPanel(props: Props) {
</div>
}
size="small"
show={filtered.length === 0}
show={filteredList.length === 0}
>
<TimeTable
rows={filtered}
rows={filteredList}
referenceLines={referenceLines}
renderPopup
onRowClick={onRowClick}
onRowClick={showDetailsModal}
additionalHeight={additionalHeight}
onJump={jump}
sortBy={sortBy}
sortAscending={sortAscending}
// activeIndex={lastIndex}
onJump={(row: any) => {
setPauseSync(true);
devTools.update(INDEX_KEY, { index: filteredList.indexOf(row) });
jump(row.time);
}}
activeIndex={activeIndex}
>
{[
// {
@ -348,28 +382,24 @@ function NetworkPanel(props: Props) {
label: 'Status',
dataKey: 'status',
width: 70,
onClick: handleSort,
},
{
label: 'Type',
dataKey: 'type',
width: 90,
render: renderType,
onClick: handleSort,
},
{
label: 'Name',
width: 240,
dataKey: 'name',
render: renderName,
onClick: handleSort,
},
{
label: 'Size',
width: 80,
dataKey: 'decodedBodySize',
render: renderSize,
onClick: handleSort,
hidden: activeTab === XHR,
},
{
@ -377,7 +407,6 @@ function NetworkPanel(props: Props) {
width: 80,
dataKey: 'duration',
render: renderDuration,
onClick: handleSort,
},
]}
</TimeTable>
@ -391,9 +420,12 @@ function NetworkPanel(props: Props) {
export default connectPlayer((state: any) => ({
location: state.location,
resources: state.resourceList,
fetchList: state.fetchList.map((i: any) => Resource({ ...i.toJS(), type: TYPES.XHR })),
fetchList: state.fetchList.map((i: any) =>
Resource({ ...i.toJS(), type: TYPES.XHR, time: i.time < 0 ? 0 : i.time })
),
domContentLoadedTime: state.domContentLoadedTime,
loadTime: state.loadTime,
time: state.time,
playing: state.playing,
domBuildingTime: state.domBuildingTime,
}))(NetworkPanel);

View file

@ -0,0 +1,203 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { hideHint } from 'Duck/components/player';
import { Tooltip, Tabs, Input, NoContent, Icon, Toggler } from 'UI';
import { getRE } from 'App/utils';
import { List, CellMeasurer, CellMeasurerCache, AutoSizer } from 'react-virtualized';
import BottomBlock from '../BottomBlock';
import { connectPlayer, jump } from 'Player';
import { useModal } from 'App/components/Modal';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import { DATADOG, SENTRY, STACKDRIVER, typeList } from 'Types/session/stackEvent';
import { connect } from 'react-redux';
import StackEventRow from 'Shared/DevTools/StackEventRow';
import StackEventModal from '../StackEventModal';
let timeOut: any = null;
const TIMEOUT_DURATION = 5000;
const INDEX_KEY = 'stackEvent';
const ALL = 'ALL';
const TABS = [ALL, ...typeList].map((tab) => ({ text: tab, key: tab }));
interface Props {
list: any;
hideHint: any;
time: any;
}
function StackEventPanel(props: Props) {
const { list, time } = props;
const additionalHeight = 0;
const {
sessionStore: { devTools },
} = useStore();
const { showModal } = useModal();
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
const [filteredList, setFilteredList] = useState([]);
const filter = useObserver(() => devTools[INDEX_KEY].filter);
const activeTab = useObserver(() => devTools[INDEX_KEY].activeTab);
const activeIndex = useObserver(() => devTools[INDEX_KEY].index);
const [pauseSync, setPauseSync] = useState(activeIndex > 0);
const synRef: any = useRef({});
synRef.current = {
pauseSync,
activeIndex,
};
const _list = React.useRef();
const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab });
const onFilterChange = ({ target: { value } }: any) => {
devTools.update(INDEX_KEY, { filter: value });
};
const getCurrentIndex = () => {
return filteredList.filter((item: any) => item.time <= time).length - 1;
};
const removePause = () => {
clearTimeout(timeOut);
setIsDetailsModalActive(false);
timeOut = setTimeout(() => {
devTools.update(INDEX_KEY, { index: getCurrentIndex() });
setPauseSync(false);
}, TIMEOUT_DURATION);
};
useEffect(() => {
const currentIndex = getCurrentIndex();
if (currentIndex !== activeIndex && !pauseSync) {
devTools.update(INDEX_KEY, { index: currentIndex });
}
}, [time]);
const onMouseLeave = () => {
if (isDetailsModalActive) return;
removePause();
};
React.useMemo(() => {
const filterRE = getRE(filter, 'i');
let list = props.list;
list = list.filter(
({ name, source }: any) =>
(!!filter ? filterRE.test(name) : true) && (activeTab === ALL || activeTab === source)
);
setFilteredList(list);
}, [filter, activeTab]);
const tabs = useMemo(() => {
return TABS.filter(({ key }) => key === ALL || list.some(({ source }: any) => key === source));
}, []);
const cache = new CellMeasurerCache({
fixedWidth: true,
keyMapper: (index: number) => filteredList[index],
});
const showDetails = (item: any) => {
setIsDetailsModalActive(true);
showModal(<StackEventModal event={item} />, { right: true, onClose: removePause });
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
setPauseSync(true);
};
const _rowRenderer = ({ index, key, parent, style }: any) => {
const item = filteredList[index];
return (
// @ts-ignore
<CellMeasurer cache={cache} columnIndex={0} key={key} rowIndex={index} parent={parent}>
{() => (
<StackEventRow
isActive={activeIndex === index}
style={style}
key={item.key}
event={item}
onJump={() => {
setPauseSync(true);
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
jump(item.time);
}}
onClick={() => showDetails(item)}
/>
)}
</CellMeasurer>
);
};
useEffect(() => {
if (_list.current) {
// @ts-ignore
_list.current.scrollToRow(activeIndex);
}
}, [activeIndex]);
return (
<BottomBlock
style={{ height: 300 + additionalHeight + 'px' }}
onMouseEnter={() => setPauseSync(true)}
onMouseLeave={onMouseLeave}
>
<BottomBlock.Header>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Stack Events</span>
<Tabs tabs={tabs} active={activeTab} onClick={onTabClick} border={false} />
</div>
<Input
className="input-small h-8"
placeholder="Filter by keyword"
icon="search"
iconPosition="left"
name="filter"
height={28}
onChange={onFilterChange}
value={filter}
/>
</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" />
No Data
</div>
}
size="small"
show={filteredList.length === 0}
>
<AutoSizer>
{({ height, width }: any) => (
<List
ref={_list}
deferredMeasurementCache={cache}
overscanRowCount={5}
rowCount={Math.ceil(filteredList.length || 1)}
rowHeight={cache.rowHeight}
rowRenderer={_rowRenderer}
width={width}
height={height}
scrollToAlignment="center"
/>
)}
</AutoSizer>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
);
}
export default connect(
(state: any) => ({
hintIsHidden:
state.getIn(['components', 'player', 'hiddenHints', 'stack']) ||
!state.getIn(['site', 'list']).some((s: any) => s.stackIntegrations),
}),
{ hideHint }
)(
connectPlayer((state: any) => ({
list: state.stackList,
time: state.time,
}))(StackEventPanel)
);

View file

@ -0,0 +1 @@
export { default } from './StackEventPanel';

View file

@ -3,21 +3,18 @@ import JumpButton from '../JumpButton';
import { Icon } from 'UI';
import cn from 'classnames';
import { OPENREPLAY, SENTRY, DATADOG, STACKDRIVER } from 'Types/session/stackEvent';
import { useModal } from 'App/components/Modal';
import StackEventModal from '../StackEventModal';
interface Props {
event: any;
onJump: any;
style?: any;
isActive?: boolean;
onClick?: any;
}
function StackEventRow(props: Props) {
const { event, onJump } = props;
const { event, onJump, style, isActive } = props;
let message = event.payload[0] || '';
message = typeof message === 'string' ? message : JSON.stringify(message);
const onClickDetails = () => {
showModal(<StackEventModal event={event} />, { right: true });
};
const { showModal } = useModal();
const iconProps: any = React.useMemo(() => {
const { source } = event;
@ -30,11 +27,13 @@ function StackEventRow(props: Props) {
return (
<div
style={style}
data-scroll-item={event.isRed()}
onClick={onClickDetails}
onClick={props.onClick}
className={cn(
'group flex items-center py-2 px-4 border-b cursor-pointer relative',
'hover:bg-active-blue'
'hover:bg-active-blue',
{ 'bg-teal-light': isActive }
)}
>
<div className={cn('mr-auto flex items-start')}>

View file

@ -72,8 +72,6 @@ type Props = {
hoverable?: boolean;
onRowClick?: (row: any, index: number) => void;
onJump?: (time: any) => void;
sortBy?: string;
sortAscending?: boolean;
};
type TimeLineInfo = {
@ -145,8 +143,19 @@ export default class TimeTable extends React.PureComponent<Props, State> {
scroller = React.createRef<List>();
autoScroll = true;
componentDidMount() {
if (this.scroller.current) {
// componentDidMount() {
// if (this.scroller.current) {
// this.scroller.current.scrollToRow(this.props.activeIndex);
// }
// }
adjustScroll(prevActiveIndex: number) {
if (
this.props.activeIndex &&
this.props.activeIndex >= 0 &&
prevActiveIndex !== this.props.activeIndex &&
this.scroller.current
) {
this.scroller.current.scrollToRow(this.props.activeIndex);
}
}
@ -161,14 +170,8 @@ export default class TimeTable extends React.PureComponent<Props, State> {
...computeTimeLine(this.props.rows, this.state.firstVisibleRowIndex, this.visibleCount),
});
}
if (
this.props.activeIndex &&
this.props.activeIndex >= 0 &&
prevProps.activeIndex !== this.props.activeIndex &&
this.scroller.current
) {
this.scroller.current.scrollToRow(this.props.activeIndex);
}
// this.adjustScroll(prevProps.activeIndex);
}
onScroll = ({
@ -190,7 +193,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
onJump = (index: any) => {
if (this.props.onJump) {
this.props.onJump(this.props.rows[index].time);
this.props.onJump(this.props.rows[index]);
}
};
@ -203,23 +206,29 @@ export default class TimeTable extends React.PureComponent<Props, State> {
<div
style={rowStyle}
key={key}
className={cn('border-b border-color-gray-light-shade group items-center', stl.row, {
[stl.hoverable]: hoverable,
'error color-red': !!row.isRed && row.isRed(),
'cursor-pointer': typeof onRowClick === 'function',
[stl.activeRow]: activeIndex === index,
// [stl.inactiveRow]: !activeIndex || index > activeIndex,
})}
className={cn(
'dev-row border-b border-color-gray-light-shade group items-center',
stl.row,
{
[stl.hoverable]: hoverable,
'error color-red': !!row.isRed && row.isRed(),
'cursor-pointer': typeof onRowClick === 'function',
[stl.activeRow]: activeIndex === index,
// [stl.inactiveRow]: !activeIndex || index > activeIndex,
}
)}
onClick={typeof onRowClick === 'function' ? () => onRowClick(row, index) : undefined}
id="table-row"
>
{columns.filter((i: any) => !i.hidden).map(({ dataKey, render, width }) => (
<div className={stl.cell} style={{ width: `${width}px` }}>
{render
? render(row)
: row[dataKey || ''] || <i className="color-gray-light">{'empty'}</i>}
</div>
))}
{columns
.filter((i: any) => !i.hidden)
.map(({ dataKey, render, width }) => (
<div className={stl.cell} style={{ width: `${width}px` }}>
{render
? render(row)
: row[dataKey || ''] || <i className="color-gray-light">{'empty'}</i>}
</div>
))}
<div className={cn('relative flex-1 flex', stl.timeBarWrapper)}>
<BarRow resource={row} timestart={timestart} timewidth={timewidth} popup={renderPopup} />
</div>
@ -270,8 +279,6 @@ export default class TimeTable extends React.PureComponent<Props, State> {
referenceLines = [],
additionalHeight = 0,
activeIndex,
sortBy = '',
sortAscending = true,
} = this.props;
const columns = this.props.children.filter((i: any) => !i.hidden);
const { timewidth, timestart } = this.state;
@ -324,10 +331,9 @@ export default class TimeTable extends React.PureComponent<Props, State> {
'cursor-pointer': typeof onClick === 'function',
})}
style={{ width: `${width}px` }}
onClick={() => this.onColumnClick(dataKey, onClick)}
// onClick={() => this.onColumnClick(dataKey, onClick)}
>
<span>{label}</span>
{!!sortBy && sortBy === dataKey && <Icon name={ sortAscending ? "caret-down-fill" : "caret-up-fill" } className="ml-1" />}
</div>
))}
</div>
@ -360,6 +366,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
<AutoSizer disableHeight>
{({ width }: { width: number }) => (
<List
scrollToIndex={this.props.activeIndex || 0}
ref={this.scroller}
className={stl.list}
height={this.tableHeight + additionalHeight}
@ -369,7 +376,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
rowHeight={ROW_HEIGHT}
rowRenderer={this.renderRow}
onScroll={this.onScroll}
scrollToAlignment="start"
scrollToAlignment="center"
forceUpdateProp={timestart | timewidth | (activeIndex || 0)}
/>
)}

View file

@ -4,6 +4,7 @@ import { Button } from 'UI';
import FetchPluginMessage from './components/FetchPluginMessage';
import { TYPES } from 'Types/session/resource';
import FetchTabs from './components/FetchTabs/FetchTabs';
import { useStore } from 'App/mstore';
interface Props {
resource: any;
@ -16,6 +17,9 @@ function FetchDetailsModal(props: Props) {
const [first, setFirst] = useState(false);
const [last, setLast] = useState(false);
const isXHR = resource.type === TYPES.XHR || resource.type === TYPES.FETCH;
const {
sessionStore: { devTools },
} = useStore();
useEffect(() => {
const index = rows.indexOf(resource);
@ -28,6 +32,7 @@ function FetchDetailsModal(props: Props) {
const index = rows.indexOf(resource);
if (index > 0) {
setResource(rows[index - 1]);
devTools.update('network', { index: index - 1 })
}
};
@ -35,6 +40,7 @@ function FetchDetailsModal(props: Props) {
const index = rows.indexOf(resource);
if (index < rows.length - 1) {
setResource(rows[index + 1]);
devTools.update('network', { index: index + 1 })
}
};

View file

@ -5,75 +5,105 @@ import Session from './types/session';
import Record, { LAST_7_DAYS } from 'Types/app/period';
class UserFilter {
endDate: number = new Date().getTime();
startDate: number = new Date().getTime() - 24 * 60 * 60 * 1000;
rangeName: string = LAST_7_DAYS;
filters: any = [];
page: number = 1;
limit: number = 10;
period: any = Record({ rangeName: LAST_7_DAYS });
endDate: number = new Date().getTime();
startDate: number = new Date().getTime() - 24 * 60 * 60 * 1000;
rangeName: string = LAST_7_DAYS;
filters: any = [];
page: number = 1;
limit: number = 10;
period: any = Record({ rangeName: LAST_7_DAYS });
constructor() {
makeAutoObservable(this, {
page: observable,
update: action,
});
constructor() {
makeAutoObservable(this, {
page: observable,
update: action,
});
}
update(key: string, value: any) {
// @ts-ignore
this[key] = value;
if (key === 'period') {
this.startDate = this.period.start;
this.endDate = this.period.end;
}
}
update(key: string, value: any) {
this[key] = value;
setFilters(filters: any[]) {
this.filters = filters;
}
if (key === 'period') {
this.startDate = this.period.start;
this.endDate = this.period.end;
}
}
setPage(page: number) {
this.page = page;
}
setFilters(filters: any[]) {
this.filters = filters;
}
toJson() {
return {
endDate: this.period.end,
startDate: this.period.start,
filters: this.filters.map(filterMap),
page: this.page,
limit: this.limit,
};
}
}
setPage(page: number) {
this.page = page;
}
interface BaseDevState {
index: number;
filter: string;
activeTab: string;
isError: boolean;
}
toJson() {
return {
endDate: this.period.end,
startDate: this.period.start,
filters: this.filters.map(filterMap),
page: this.page,
limit: this.limit,
};
}
class DevTools {
network: BaseDevState;
stackEvent: BaseDevState;
console: BaseDevState;
constructor() {
this.network = { index: 0, filter: '', activeTab: 'ALL', isError: false };
this.stackEvent = { index: 0, filter: '', activeTab: 'ALL', isError: false };
this.console = { index: 0, filter: '', activeTab: 'ALL', isError: false };
makeAutoObservable(this, {
update: action,
});
}
update(key: string, value: any) {
// @ts-ignore
this[key] = Object.assign(this[key], value);
}
}
export default class SessionStore {
userFilter: UserFilter = new UserFilter();
userFilter: UserFilter = new UserFilter();
devTools: DevTools = new DevTools();
constructor() {
makeAutoObservable(this, {
userFilter: observable,
constructor() {
makeAutoObservable(this, {
userFilter: observable,
devTools: observable,
});
}
resetUserFilter() {
this.userFilter = new UserFilter();
}
getSessions(filter: any): Promise<any> {
return new Promise((resolve, reject) => {
sessionService
.getSessions(filter.toJson())
.then((response: any) => {
resolve({
sessions: response.sessions.map((session: any) => new Session().fromJson(session)),
total: response.total,
});
})
.catch((error: any) => {
reject(error);
});
}
resetUserFilter() {
this.userFilter = new UserFilter();
}
getSessions(filter: any): Promise<any> {
return new Promise((resolve, reject) => {
sessionService
.getSessions(filter.toJson())
.then((response: any) => {
resolve({
sessions: response.sessions.map((session: any) => new Session().fromJson(session)),
total: response.total,
});
})
.catch((error: any) => {
reject(error);
});
});
}
});
}
}

View file

@ -132,7 +132,6 @@ export default class MessageDistributor extends StatedScreen {
exceptions: session.errors.toJSON(),
})
/* === */
this.loadMessages();
}

View file

@ -355,4 +355,8 @@ p {
width: 80px;
height: 80px;
transform: rotate(45deg);
}
.dev-row {
transition: all 0.5s;
}

View file

@ -91,6 +91,7 @@
"@types/react-dom": "^18.0.4",
"@types/react-redux": "^7.1.24",
"@types/react-router-dom": "^5.3.3",
"@types/react-virtualized": "^9.21.21",
"@typescript-eslint/eslint-plugin": "^5.24.0",
"@typescript-eslint/parser": "^5.24.0",
"autoprefixer": "^10.4.7",