Merge pull request #679 from Aeolun/feature/more-consistent-player-experience
feat: add consistent timestamps to (almost) all items in the player ui
This commit is contained in:
commit
103540fb1e
52 changed files with 2199 additions and 1854 deletions
|
|
@ -82,7 +82,7 @@ class AutoComplete extends React.PureComponent {
|
|||
onInputChange = ({ target: { value } }) => {
|
||||
changed = true;
|
||||
this.setState({ query: value, updated: true })
|
||||
const _value = value.trim();
|
||||
const _value = value ? value.trim() : undefined;
|
||||
if (_value !== '' && _value !== ' ') {
|
||||
this.debouncedRequestValues(_value)
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ class AutoComplete extends React.PureComponent {
|
|||
value = pasted ? this.hiddenInput.value : value;
|
||||
const { onSelect, name } = this.props;
|
||||
if (value !== this.props.value) {
|
||||
const _value = value.trim();
|
||||
const _value = value ? value.trim() : undefined;
|
||||
onSelect(null, {name, value: _value});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,101 +8,142 @@ import { WIDGET_MAP } from 'Types/dashboard';
|
|||
import Title from './Title';
|
||||
import stl from './widgetHOC.module.css';
|
||||
|
||||
export default (
|
||||
widgetKey,
|
||||
panelProps = {},
|
||||
wrapped = true,
|
||||
allowedFilters = [],
|
||||
) => BaseComponent =>
|
||||
@connect((state, props) => {
|
||||
const compare = props && props.compare;
|
||||
const key = compare ? '_' + widgetKey : widgetKey;
|
||||
export default (widgetKey, panelProps = {}, wrapped = true, allowedFilters = []) =>
|
||||
(BaseComponent) => {
|
||||
@connect(
|
||||
(state, props) => {
|
||||
const compare = props && props.compare;
|
||||
const key = compare ? '_' + widgetKey : widgetKey;
|
||||
|
||||
return {
|
||||
loading: state.getIn([ 'dashboard', 'fetchWidget', key, 'loading' ]),
|
||||
data: state.getIn([ 'dashboard', key ]),
|
||||
comparing: state.getIn([ 'dashboard', 'comparing' ]),
|
||||
filtersSize: state.getIn([ 'dashboard', 'filters' ]).size,
|
||||
filters: state.getIn([ 'dashboard', compare ? 'filtersCompare' : 'filters' ]),
|
||||
period: state.getIn([ 'dashboard', compare ? 'periodCompare' : 'period' ]), //TODO: filters
|
||||
platform: state.getIn([ 'dashboard', 'platform' ]),
|
||||
// appearance: state.getIn([ 'user', 'account', 'appearance' ]),
|
||||
return {
|
||||
loading: state.getIn(['dashboard', 'fetchWidget', key, 'loading']),
|
||||
data: state.getIn(['dashboard', key]),
|
||||
comparing: state.getIn(['dashboard', 'comparing']),
|
||||
filtersSize: state.getIn(['dashboard', 'filters']).size,
|
||||
filters: state.getIn(['dashboard', compare ? 'filtersCompare' : 'filters']),
|
||||
period: state.getIn(['dashboard', compare ? 'periodCompare' : 'period']), //TODO: filters
|
||||
platform: state.getIn(['dashboard', 'platform']),
|
||||
// appearance: state.getIn([ 'user', 'account', 'appearance' ]),
|
||||
|
||||
dataCompare: state.getIn([ 'dashboard', '_' + widgetKey ]), // only for overview
|
||||
loadingCompare: state.getIn([ 'dashboard', 'fetchWidget', '_' + widgetKey, 'loading' ]),
|
||||
filtersCompare: state.getIn([ 'dashboard', 'filtersCompare' ]),
|
||||
periodCompare: state.getIn([ 'dashboard', 'periodCompare' ]), //TODO: filters
|
||||
}
|
||||
}, {
|
||||
fetchWidget,
|
||||
// updateAppearance,
|
||||
})
|
||||
class WidgetWrapper extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const params = panelProps.customParams ? panelProps.customParams(this.props.period.rangeName) : {};
|
||||
if(props.testId) {
|
||||
params.testId = parseInt(props.testId);
|
||||
}
|
||||
params.compare = this.props.compare;
|
||||
const filters = allowedFilters.length > 0 ? props.filters.filter(f => allowedFilters.includes(f.key)) : props.filters;
|
||||
props.fetchWidget(widgetKey, props.period, props.platform, params, filters);
|
||||
}
|
||||
dataCompare: state.getIn(['dashboard', '_' + widgetKey]), // only for overview
|
||||
loadingCompare: state.getIn(['dashboard', 'fetchWidget', '_' + widgetKey, 'loading']),
|
||||
filtersCompare: state.getIn(['dashboard', 'filtersCompare']),
|
||||
periodCompare: state.getIn(['dashboard', 'periodCompare']), //TODO: filters
|
||||
};
|
||||
},
|
||||
{
|
||||
fetchWidget,
|
||||
// updateAppearance,
|
||||
}
|
||||
)
|
||||
class WidgetWrapper extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const params = panelProps.customParams
|
||||
? panelProps.customParams(this.props.period.rangeName)
|
||||
: {};
|
||||
if (props.testId) {
|
||||
params.testId = parseInt(props.testId);
|
||||
}
|
||||
params.compare = this.props.compare;
|
||||
const filters =
|
||||
allowedFilters.length > 0
|
||||
? props.filters.filter((f) => allowedFilters.includes(f.key))
|
||||
: props.filters;
|
||||
props.fetchWidget(widgetKey, props.period, props.platform, params, filters);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.period !== this.props.period ||
|
||||
prevProps.platform !== this.props.platform ||
|
||||
prevProps.filters.size !== this.props.filters.size) {
|
||||
const params = panelProps.customParams ? panelProps.customParams(this.props.period.rangeName) : {};
|
||||
if(this.props.testId) {
|
||||
params.testId = parseInt(this.props.testId);
|
||||
}
|
||||
params.compare = this.props.compare;
|
||||
const filters = allowedFilters.length > 0 ? this.props.filters.filter(f => allowedFilters.includes(f.key)) : this.props.filters;
|
||||
this.props.fetchWidget(widgetKey, this.props.period, this.props.platform, params, filters);
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.period !== this.props.period ||
|
||||
prevProps.platform !== this.props.platform ||
|
||||
prevProps.filters.size !== this.props.filters.size
|
||||
) {
|
||||
const params = panelProps.customParams
|
||||
? panelProps.customParams(this.props.period.rangeName)
|
||||
: {};
|
||||
if (this.props.testId) {
|
||||
params.testId = parseInt(this.props.testId);
|
||||
}
|
||||
params.compare = this.props.compare;
|
||||
const filters =
|
||||
allowedFilters.length > 0
|
||||
? this.props.filters.filter((f) => allowedFilters.includes(f.key))
|
||||
: this.props.filters;
|
||||
this.props.fetchWidget(
|
||||
widgetKey,
|
||||
this.props.period,
|
||||
this.props.platform,
|
||||
params,
|
||||
filters
|
||||
);
|
||||
}
|
||||
|
||||
// handling overview widgets
|
||||
if ((!prevProps.comparing || prevProps.periodCompare !== this.props.periodCompare || prevProps.filtersCompare.size !== this.props.filtersCompare.size) &&
|
||||
this.props.comparing && this.props.isOverview
|
||||
) {
|
||||
const params = panelProps.customParams ? panelProps.customParams(this.props.period.rangeName) : {};
|
||||
params.compare = true;
|
||||
const filtersCompare = allowedFilters.length > 0 ? this.props.filtersCompare.filter(f => allowedFilters.includes(f.key)) : this.props.filtersCompare;
|
||||
this.props.fetchWidget(widgetKey, this.props.periodCompare, this.props.platform, params, filtersCompare);
|
||||
}
|
||||
}
|
||||
// handling overview widgets
|
||||
if (
|
||||
(!prevProps.comparing ||
|
||||
prevProps.periodCompare !== this.props.periodCompare ||
|
||||
prevProps.filtersCompare.size !== this.props.filtersCompare.size) &&
|
||||
this.props.comparing &&
|
||||
this.props.isOverview
|
||||
) {
|
||||
const params = panelProps.customParams
|
||||
? panelProps.customParams(this.props.period.rangeName)
|
||||
: {};
|
||||
params.compare = true;
|
||||
const filtersCompare =
|
||||
allowedFilters.length > 0
|
||||
? this.props.filtersCompare.filter((f) => allowedFilters.includes(f.key))
|
||||
: this.props.filtersCompare;
|
||||
this.props.fetchWidget(
|
||||
widgetKey,
|
||||
this.props.periodCompare,
|
||||
this.props.platform,
|
||||
params,
|
||||
filtersCompare
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleRemove = () => {
|
||||
// const { appearance } = this.props;
|
||||
// this.props.updateAppearance(appearance.setIn([ 'dashboard', widgetKey ], false));
|
||||
}
|
||||
handleRemove = () => {
|
||||
// const { appearance } = this.props;
|
||||
// this.props.updateAppearance(appearance.setIn([ 'dashboard', widgetKey ], false));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { comparing, compare } = this.props;
|
||||
render() {
|
||||
const { comparing, compare } = this.props;
|
||||
|
||||
return (
|
||||
wrapped ?
|
||||
<div className={cn(stl.wrapper, { [stl.comparing] : comparing })}>
|
||||
<div
|
||||
className={ cn(stl.panel, "flex flex-col relative", {
|
||||
[ stl.fullwidth ]: panelProps.fullwidth,
|
||||
[ stl.fitContent ]: panelProps.fitContent,
|
||||
[ stl.minHeight ]: !panelProps.fitContent,
|
||||
}) }
|
||||
>
|
||||
<div className="flex items-center mb-2" >
|
||||
{comparing && <div className={cn(stl.circle, { 'bg-tealx' : !compare, 'bg-teal' : compare})} />}
|
||||
<Title title={ panelProps.name ? panelProps.name : WIDGET_MAP[ widgetKey ].name } />
|
||||
{ <CloseButton className={ cn(stl.closeButton, 'ml-auto') } onClick={ this.handleRemove } size="17" /> }
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col">
|
||||
<BaseComponent { ...this.props } />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<BaseComponent { ...this.props } />
|
||||
)
|
||||
}
|
||||
}
|
||||
return wrapped ? (
|
||||
<div className={cn(stl.wrapper, { [stl.comparing]: comparing })}>
|
||||
<div
|
||||
className={cn(stl.panel, 'flex flex-col relative', {
|
||||
[stl.fullwidth]: panelProps.fullwidth,
|
||||
[stl.fitContent]: panelProps.fitContent,
|
||||
[stl.minHeight]: !panelProps.fitContent,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center mb-2">
|
||||
{comparing && (
|
||||
<div className={cn(stl.circle, { 'bg-tealx': !compare, 'bg-teal': compare })} />
|
||||
)}
|
||||
<Title title={panelProps.name ? panelProps.name : WIDGET_MAP[widgetKey].name} />
|
||||
{
|
||||
<CloseButton
|
||||
className={cn(stl.closeButton, 'ml-auto')}
|
||||
onClick={this.handleRemove}
|
||||
size="17"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col">
|
||||
<BaseComponent {...this.props} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<BaseComponent {...this.props} />
|
||||
);
|
||||
}
|
||||
}
|
||||
return WidgetWrapper;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ function SeriesName(props: Props) {
|
|||
onFocus={() => setEditing(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-base h-8 flex items-center border-transparent">{name.trim() === '' ? 'Seriess ' + (seriesIndex + 1) : name }</div>
|
||||
<div className="text-base h-8 flex items-center border-transparent">{name && name.trim() === '' ? 'Series ' + (seriesIndex + 1) : name }</div>
|
||||
)}
|
||||
|
||||
<div className="ml-3 cursor-pointer" onClick={() => setEditing(true)}><Icon name="pencil" size="14" /></div>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ function WidgetName(props: Props) {
|
|||
const onBlur = (nameInput?: string) => {
|
||||
setEditing(false)
|
||||
const toUpdate = nameInput || name
|
||||
props.onUpdate(toUpdate.trim() === '' ? 'New Widget' : toUpdate)
|
||||
props.onUpdate(toUpdate && toUpdate.trim() === '' ? 'New Widget' : toUpdate)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,16 @@ import { Button, Modal, Form, Icon, Checkbox, Input } from 'UI';
|
|||
import styles from './funnelSaveModal.module.css';
|
||||
import { edit, save, fetchList as fetchFunnelsList } from 'Duck/funnels';
|
||||
|
||||
@connect(state => ({
|
||||
filter: state.getIn(['search', 'instance']),
|
||||
funnel: state.getIn(['funnels', 'instance']),
|
||||
loading: state.getIn([ 'funnels', 'saveRequest', 'loading' ]) ||
|
||||
state.getIn([ 'funnels', 'updateRequest', 'loading' ]),
|
||||
}), { edit, save, fetchFunnelsList })
|
||||
@connect(
|
||||
(state) => ({
|
||||
filter: state.getIn(['search', 'instance']),
|
||||
funnel: state.getIn(['funnels', 'instance']),
|
||||
loading:
|
||||
state.getIn(['funnels', 'saveRequest', 'loading']) ||
|
||||
state.getIn(['funnels', 'updateRequest', 'loading']),
|
||||
}),
|
||||
{ edit, save, fetchFunnelsList }
|
||||
)
|
||||
export default class FunnelSaveModal extends React.PureComponent {
|
||||
state = { name: 'Untitled', isPublic: false };
|
||||
static getDerivedStateFromProps(props) {
|
||||
|
|
@ -26,36 +30,33 @@ export default class FunnelSaveModal extends React.PureComponent {
|
|||
this.props.edit({ name: value });
|
||||
};
|
||||
|
||||
onChangeOption = (e, { checked, name }) => this.props.edit({ [ name ]: checked })
|
||||
onChangeOption = (e, { checked, name }) => this.props.edit({ [name]: checked });
|
||||
|
||||
onSave = () => {
|
||||
const { funnel, filter } = this.props;
|
||||
if (funnel.name.trim() === '') return;
|
||||
this.props.save(funnel).then(function() {
|
||||
this.props.fetchFunnelsList();
|
||||
this.props.closeHandler();
|
||||
}.bind(this));
|
||||
}
|
||||
if (funnel.name && funnel.name.trim() === '') return;
|
||||
this.props.save(funnel).then(
|
||||
function () {
|
||||
this.props.fetchFunnelsList();
|
||||
this.props.closeHandler();
|
||||
}.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
show,
|
||||
closeHandler,
|
||||
loading,
|
||||
funnel
|
||||
} = this.props;
|
||||
|
||||
const { show, closeHandler, loading, funnel } = this.props;
|
||||
|
||||
return (
|
||||
<Modal size="small" open={ show } onClose={this.props.closeHandler}>
|
||||
<Modal.Header className={ styles.modalHeader }>
|
||||
<div>{ 'Save Funnel' }</div>
|
||||
<Icon
|
||||
<Modal size="small" open={show} onClose={this.props.closeHandler}>
|
||||
<Modal.Header className={styles.modalHeader}>
|
||||
<div>{'Save Funnel'}</div>
|
||||
<Icon
|
||||
role="button"
|
||||
tabIndex="-1"
|
||||
color="gray-dark"
|
||||
size="14"
|
||||
name="close"
|
||||
onClick={ closeHandler }
|
||||
onClick={closeHandler}
|
||||
/>
|
||||
</Modal.Header>
|
||||
|
||||
|
|
@ -64,11 +65,11 @@ export default class FunnelSaveModal extends React.PureComponent {
|
|||
<Form.Field>
|
||||
<label>{'Title:'}</label>
|
||||
<Input
|
||||
autoFocus={ true }
|
||||
className={ styles.name }
|
||||
autoFocus={true}
|
||||
className={styles.name}
|
||||
name="name"
|
||||
value={ funnel.name }
|
||||
onChange={ this.onNameChange }
|
||||
value={funnel.name}
|
||||
onChange={this.onNameChange}
|
||||
placeholder="Title"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
|
@ -79,11 +80,14 @@ export default class FunnelSaveModal extends React.PureComponent {
|
|||
name="isPublic"
|
||||
className="font-medium"
|
||||
type="checkbox"
|
||||
checked={ funnel.isPublic }
|
||||
onClick={ this.onChangeOption }
|
||||
className="mr-3"
|
||||
checked={funnel.isPublic}
|
||||
onClick={this.onChangeOption}
|
||||
className="mr-3"
|
||||
/>
|
||||
<div className="flex items-center cursor-pointer" onClick={ () => this.props.edit({ 'isPublic' : !funnel.isPublic }) }>
|
||||
<div
|
||||
className="flex items-center cursor-pointer"
|
||||
onClick={() => this.props.edit({ isPublic: !funnel.isPublic })}
|
||||
>
|
||||
<Icon name="user-friends" size="16" />
|
||||
<span className="ml-2"> Team Visible</span>
|
||||
</div>
|
||||
|
|
@ -91,16 +95,16 @@ export default class FunnelSaveModal extends React.PureComponent {
|
|||
</Form.Field>
|
||||
</Form>
|
||||
</Modal.Content>
|
||||
<Modal.Footer className="">
|
||||
<Modal.Footer className="">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={ this.onSave }
|
||||
loading={ loading }
|
||||
onClick={this.onSave}
|
||||
loading={loading}
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{ funnel.exists() ? 'Modify' : 'Save' }
|
||||
{funnel.exists() ? 'Modify' : 'Save'}
|
||||
</Button>
|
||||
<Button onClick={ closeHandler }>{ 'Cancel' }</Button>
|
||||
<Button onClick={closeHandler}>{'Cancel'}</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,82 +10,85 @@ import TimeTable from 'Components/Session_/TimeTable';
|
|||
import FetchDetails from 'Components/Session_/Fetch/FetchDetails';
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
label: "Status",
|
||||
dataKey: 'status',
|
||||
width: 70,
|
||||
}, {
|
||||
label: "Method",
|
||||
dataKey: 'method',
|
||||
width: 60,
|
||||
}, {
|
||||
label: "url",
|
||||
width: 130,
|
||||
render: (r) =>
|
||||
<Popup
|
||||
content={ <div className={ cls.popupNameContent }>{ r.url }</div> }
|
||||
size="mini"
|
||||
position="right center"
|
||||
>
|
||||
<div className={ cls.popupNameTrigger }>{ r.url }</div>
|
||||
</Popup>
|
||||
},
|
||||
{
|
||||
label: "Size",
|
||||
width: 60,
|
||||
render: (r) => `${r.body.length}`,
|
||||
},
|
||||
{
|
||||
label: "Time",
|
||||
width: 80,
|
||||
render: (r) => `${r.duration}ms`,
|
||||
}
|
||||
{
|
||||
label: 'Status',
|
||||
dataKey: 'status',
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
label: 'Method',
|
||||
dataKey: 'method',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
label: 'url',
|
||||
width: 130,
|
||||
render: (r) => (
|
||||
<Popup
|
||||
content={<div className={cls.popupNameContent}>{r.url}</div>}
|
||||
size="mini"
|
||||
position="right center"
|
||||
>
|
||||
<div className={cls.popupNameTrigger}>{r.url}</div>
|
||||
</Popup>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Size',
|
||||
width: 60,
|
||||
render: (r) => `${r.body.length}`,
|
||||
},
|
||||
{
|
||||
label: 'Time',
|
||||
width: 80,
|
||||
render: (r) => `${r.duration}ms`,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
function Network({ player }) {
|
||||
const [ current, setCurrent ] = useState(null);
|
||||
const [ currentIndex, setCurrentIndex ] = useState(0);
|
||||
const onRowClick = useCallback((raw, index) => {
|
||||
setCurrent(raw);
|
||||
setCurrentIndex(index);
|
||||
});
|
||||
const onNextClick = useCallback(() => {
|
||||
onRowClick(player.lists[NETWORK].list[currentIndex+1], currentIndex+1)
|
||||
});
|
||||
const onPrevClick = useCallback(() => {
|
||||
onRowClick(player.lists[NETWORK].list[currentIndex-1], currentIndex-1)
|
||||
});
|
||||
const closeModal = useCallback(() => setCurrent(null)); // TODO: handle in modal
|
||||
const [current, setCurrent] = useState(null);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const onRowClick = useCallback((raw, index) => {
|
||||
setCurrent(raw);
|
||||
setCurrentIndex(index);
|
||||
});
|
||||
const onNextClick = useCallback(() => {
|
||||
onRowClick(player.lists[NETWORK].list[currentIndex + 1], currentIndex + 1);
|
||||
});
|
||||
const onPrevClick = useCallback(() => {
|
||||
onRowClick(player.lists[NETWORK].list[currentIndex - 1], currentIndex - 1);
|
||||
});
|
||||
const closeModal = useCallback(() => setCurrent(null)); // TODO: handle in modal
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlideModal
|
||||
return (
|
||||
<>
|
||||
<SlideModal
|
||||
size="middle"
|
||||
title="Network Request"
|
||||
isDisplayed={ current != null }
|
||||
content={ current &&
|
||||
<FetchDetails
|
||||
resource={ current }
|
||||
nextClick={ onNextClick }
|
||||
prevClick={ onPrevClick }
|
||||
first={ currentIndex === 0 }
|
||||
last={ currentIndex === player.lists[NETWORK].countNow - 1 }
|
||||
/>
|
||||
isDisplayed={current != null}
|
||||
content={
|
||||
current && (
|
||||
<FetchDetails
|
||||
resource={current}
|
||||
nextClick={onNextClick}
|
||||
prevClick={onPrevClick}
|
||||
first={currentIndex === 0}
|
||||
last={currentIndex === player.lists[NETWORK].countNow - 1}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClose={ closeModal }
|
||||
onClose={closeModal}
|
||||
/>
|
||||
<TimeTable
|
||||
rows={ player.lists[NETWORK].listNow }
|
||||
hoverable
|
||||
tableHeight={270}
|
||||
onRowClick={ onRowClick }
|
||||
>
|
||||
{ COLUMNS }
|
||||
</TimeTable>
|
||||
</>
|
||||
);
|
||||
<TimeTable
|
||||
rows={player.lists[NETWORK].listNow}
|
||||
hoverable
|
||||
tableHeight={270}
|
||||
onRowClick={onRowClick}
|
||||
>
|
||||
{COLUMNS}
|
||||
</TimeTable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Network);
|
||||
export default observer(Network);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { CUSTOM } from 'Player/ios/state';
|
||||
import { CUSTOM } from 'Player/ios/state';
|
||||
|
||||
import StackEvents from '../Layout/ToolPanel/StackEvents';
|
||||
|
||||
|
||||
export default observer(({ player }) =>
|
||||
<StackEvents
|
||||
stackEvents={ player.lists[CUSTOM].listNow }
|
||||
/>
|
||||
);
|
||||
export default observer(({ player }) => <StackEvents stackEvents={player.lists[CUSTOM].listNow} />);
|
||||
|
|
|
|||
|
|
@ -1,85 +0,0 @@
|
|||
import React from 'react';
|
||||
import { IconButton } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import stl from './autoscroll.module.css';
|
||||
|
||||
export default class Autoscroll extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
bottomOffset: 10,
|
||||
};
|
||||
state = {
|
||||
autoScroll: true,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.scrollableElement) return; // is necessary ?
|
||||
this.scrollableElement.addEventListener('scroll', this.scrollHandler);
|
||||
this.scrollableElement.scrollTop = this.scrollableElement.scrollHeight;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (!this.scrollableElement) return; // is necessary ?
|
||||
if (this.state.autoScroll) {
|
||||
this.scrollableElement.scrollTop = this.scrollableElement.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
scrollHandler = (e) => {
|
||||
if (!this.scrollableElement) return;
|
||||
this.setState({
|
||||
autoScroll:
|
||||
this.scrollableElement.scrollHeight - this.scrollableElement.clientHeight - this.scrollableElement.scrollTop <
|
||||
this.props.bottomOffset,
|
||||
});
|
||||
};
|
||||
|
||||
onPrevClick = () => {
|
||||
if (!this.scrollableElement) return;
|
||||
const scEl = this.scrollableElement;
|
||||
let prevItem;
|
||||
for (let i = scEl.children.length - 1; i >= 0; i--) {
|
||||
const child = scEl.children[i];
|
||||
const isScrollable = child.getAttribute('data-scroll-item') === 'true';
|
||||
if (isScrollable && child.offsetTop < scEl.scrollTop) {
|
||||
prevItem = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!prevItem) return;
|
||||
scEl.scrollTop = prevItem.offsetTop;
|
||||
};
|
||||
|
||||
onNextClick = () => {
|
||||
if (!this.scrollableElement) return;
|
||||
const scEl = this.scrollableElement;
|
||||
let nextItem;
|
||||
for (let i = 0; i < scEl.children.length; i++) {
|
||||
const child = scEl.children[i];
|
||||
const isScrollable = child.getAttribute('data-scroll-item') === 'true';
|
||||
if (isScrollable && child.offsetTop > scEl.scrollTop + 20) {
|
||||
// ?
|
||||
nextItem = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!nextItem) return;
|
||||
scEl.scrollTop = nextItem.offsetTop;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, navigation = false, children, ...props } = this.props;
|
||||
return (
|
||||
<div className={cn('relative w-full h-full', stl.wrapper)}>
|
||||
<div {...props} className={cn('relative scroll-y h-full', className)} ref={(ref) => (this.scrollableElement = ref)}>
|
||||
{children}
|
||||
</div>
|
||||
{navigation && (
|
||||
<div className={stl.navButtons}>
|
||||
<IconButton size="small" icon="chevron-up" onClick={this.onPrevClick} />
|
||||
<IconButton size="small" icon="chevron-down" onClick={this.onNextClick} className="mt-5" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
128
frontend/app/components/Session_/Autoscroll.tsx
Normal file
128
frontend/app/components/Session_/Autoscroll.tsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import { IconButton } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import stl from './autoscroll.module.css';
|
||||
|
||||
interface Props {
|
||||
autoScrollTo?: number
|
||||
children: ReactNode[]
|
||||
className?: string
|
||||
navigation?: boolean
|
||||
}
|
||||
|
||||
export default class Autoscroll extends React.PureComponent<Props, {
|
||||
autoScroll: boolean
|
||||
currentIndex?: number
|
||||
}> {
|
||||
state = {
|
||||
autoScroll: true,
|
||||
currentIndex: 0,
|
||||
};
|
||||
scrollableElement = React.createRef<HTMLDivElement>()
|
||||
|
||||
autoScroll(hard = false) {
|
||||
if (this.props.autoScrollTo !== undefined && this.props.autoScrollTo !== null && this.props.autoScrollTo >= 0) {
|
||||
// we have an element to scroll to
|
||||
this.scrollToElement(this.props.autoScrollTo, hard)
|
||||
} else if (this.scrollableElement.current) {
|
||||
// no element to scroll to, scroll to bottom
|
||||
this.scrollableElement.current.scrollTop = this.scrollableElement.current.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
scrollToElement(elementIndex: number, hard = false) {
|
||||
if (!this.scrollableElement.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.scrollableElement.current.children.length < elementIndex || elementIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = this.scrollableElement.current.children[elementIndex] as (HTMLElement | undefined)
|
||||
|
||||
if (element) {
|
||||
if (this.scrollableElement.current.scrollTo && !hard) {
|
||||
this.scrollableElement.current.scrollTo({
|
||||
left: 0,
|
||||
top: element.offsetTop,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
} else {
|
||||
this.scrollableElement.current.scrollTop = element.offsetTop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.scrollableElement.current) return; // is necessary ?
|
||||
|
||||
this.scrollableElement.current.addEventListener('scroll', this.scrollHandler);
|
||||
if (this.state.autoScroll) {
|
||||
this.setState({
|
||||
currentIndex: this.props.autoScrollTo
|
||||
})
|
||||
this.autoScroll(true)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(nextProps: Props) {
|
||||
if (!this.scrollableElement) return; // is necessary ?
|
||||
|
||||
if (this.state.autoScroll) {
|
||||
this.setState({
|
||||
currentIndex: this.props.autoScrollTo
|
||||
})
|
||||
this.autoScroll()
|
||||
}
|
||||
}
|
||||
|
||||
scrollHandler = (e) => {
|
||||
if (!this.scrollableElement) return;
|
||||
};
|
||||
|
||||
// TODO: Maybe make this handlers that allow the parent element to set a new autoscroll index
|
||||
onPrevClick = () => {
|
||||
if (!this.scrollableElement) return;
|
||||
|
||||
const newIndex = Math.max(this.state.currentIndex - 1, 0)
|
||||
this.setState({
|
||||
autoScroll: false,
|
||||
currentIndex: newIndex
|
||||
})
|
||||
this.scrollToElement(newIndex)
|
||||
};
|
||||
|
||||
onNextClick = () => {
|
||||
if (!this.scrollableElement) return;
|
||||
|
||||
const newIndex = Math.min(this.state.currentIndex + 1, this.props.children.length - 1)
|
||||
this.setState({
|
||||
autoScroll: false,
|
||||
currentIndex: newIndex
|
||||
})
|
||||
this.scrollToElement(newIndex)
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, navigation = false, children, ...props } = this.props;
|
||||
return (
|
||||
<div className={cn('relative w-full h-full', stl.wrapper)}>
|
||||
<div {...props} className={cn('relative scroll-y h-full', className)} ref={this.scrollableElement}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div className={stl.navButtons}>
|
||||
<label><input type={'checkbox'} checked={this.state.autoScroll} onChange={(e) => this.setState({ autoScroll: !this.state.autoScroll })} /> Autoscroll</label>
|
||||
{navigation && (
|
||||
<>
|
||||
<IconButton size="small" icon="chevron-up" onClick={this.onPrevClick} />
|
||||
<IconButton size="small" icon="chevron-down" onClick={this.onNextClick} className="mt-5" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ const Header = ({
|
|||
showClose = true,
|
||||
...props
|
||||
}) => (
|
||||
<div className={ cn("relative border-r border-l", stl.header) } >
|
||||
<div className={ cn("relative border-r border-l py-1", stl.header) } >
|
||||
<div className={ cn("w-full h-full flex justify-between items-center", className) } >
|
||||
<div className="w-full flex items-center justify-between">{ children }</div>
|
||||
{ showClose && <CloseButton onClick={ closeBottomBlock } size="18" className="ml-2" /> }
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const InfoLine = ({ children }) => (
|
|||
const Point = ({ label, value, display=true, color, dotColor }) => display
|
||||
? <div className={ cls.infoPoint } style={{ color }}>
|
||||
{ dotColor != null && <div className={ cn(cls.dot, `bg-${dotColor}`) } /> }
|
||||
<span className={cls.label}>{ `${label}:` }</span> { value }
|
||||
<span className={cls.label}>{ `${label}` }</span> { value }
|
||||
</div>
|
||||
: null;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@
|
|||
/* padding-right: 10px; */
|
||||
/* border: solid thin $gray-light; */
|
||||
height: 300px;
|
||||
padding-top: 2px;
|
||||
border-top: thin dashed #cccccc
|
||||
|
||||
border-top: thin dashed #cccccc;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@
|
|||
align-items: center;
|
||||
&:not(:last-child):after {
|
||||
content: '';
|
||||
margin: 0 10px;
|
||||
margin: 0 12px;
|
||||
height: 30px;
|
||||
border-right: 1px solid $gray-light-shade;
|
||||
}
|
||||
& .label {
|
||||
font-weight: 500;
|
||||
margin-right: 3px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export default class Console extends React.PureComponent {
|
|||
render() {
|
||||
const { logs, time, listNow } = this.props;
|
||||
return (
|
||||
<ConsoleContent jump={!this.props.livePlay && jump} logs={logs} lastIndex={listNow.length - 1} />
|
||||
<ConsoleContent jump={!this.props.livePlay && jump} logs={logs} lastIndex={listNow.length - 1} logsNow={listNow} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { LEVEL } from 'Types/session/log';
|
|||
import Autoscroll from '../Autoscroll';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import stl from './console.module.css';
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
const ALL = 'ALL';
|
||||
const INFO = 'INFO';
|
||||
|
|
@ -14,108 +15,113 @@ const WARNINGS = 'WARNINGS';
|
|||
const ERRORS = 'ERRORS';
|
||||
|
||||
const LEVEL_TAB = {
|
||||
[LEVEL.INFO]: INFO,
|
||||
[LEVEL.LOG]: INFO,
|
||||
[LEVEL.WARNING]: WARNINGS,
|
||||
[LEVEL.ERROR]: ERRORS,
|
||||
[LEVEL.EXCEPTION]: ERRORS,
|
||||
[LEVEL.INFO]: INFO,
|
||||
[LEVEL.LOG]: INFO,
|
||||
[LEVEL.WARNING]: WARNINGS,
|
||||
[LEVEL.ERROR]: ERRORS,
|
||||
[LEVEL.EXCEPTION]: ERRORS,
|
||||
};
|
||||
|
||||
const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({ text: tab, key: tab }));
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
const getIconProps = (level) => {
|
||||
switch (level) {
|
||||
case LEVEL.INFO:
|
||||
case LEVEL.LOG:
|
||||
return {
|
||||
name: 'console/info',
|
||||
color: 'blue2',
|
||||
};
|
||||
case LEVEL.WARN:
|
||||
case LEVEL.WARNING:
|
||||
return {
|
||||
name: 'console/warning',
|
||||
color: 'red2',
|
||||
};
|
||||
case LEVEL.ERROR:
|
||||
return {
|
||||
name: 'console/error',
|
||||
color: 'red',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
switch (level) {
|
||||
case LEVEL.INFO:
|
||||
case LEVEL.LOG:
|
||||
return {
|
||||
name: 'console/info',
|
||||
color: 'blue2',
|
||||
};
|
||||
case LEVEL.WARN:
|
||||
case LEVEL.WARNING:
|
||||
return {
|
||||
name: 'console/warning',
|
||||
color: 'red2',
|
||||
};
|
||||
case LEVEL.ERROR:
|
||||
return {
|
||||
name: 'console/error',
|
||||
color: 'red',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
function renderWithNL(s = '') {
|
||||
if (typeof s !== 'string') return '';
|
||||
return s.split('\n').map((line, i) => <div className={cn({ 'ml-20': i !== 0 })}>{line}</div>);
|
||||
if (typeof s !== 'string') return '';
|
||||
return s.split('\n').map((line, i) => <div className={cn({ 'ml-20': i !== 0 })}>{line}</div>);
|
||||
}
|
||||
|
||||
export default class ConsoleContent extends React.PureComponent {
|
||||
state = {
|
||||
filter: '',
|
||||
activeTab: ALL,
|
||||
};
|
||||
onTabClick = (activeTab) => this.setState({ activeTab });
|
||||
onFilterChange = ({ target: { value } }) => this.setState({ filter: value });
|
||||
state = {
|
||||
filter: '',
|
||||
activeTab: ALL,
|
||||
};
|
||||
onTabClick = (activeTab) => this.setState({ activeTab });
|
||||
onFilterChange = ({ target: { value } }) => this.setState({ filter: value });
|
||||
|
||||
render() {
|
||||
const { logs, isResult, additionalHeight, lastIndex } = this.props;
|
||||
const { filter, activeTab } = this.state;
|
||||
const filterRE = getRE(filter, 'i');
|
||||
const filtered = logs.filter(({ level, value }) =>
|
||||
activeTab === ALL ? filterRE.test(value) : filterRE.test(value) && LEVEL_TAB[level] === activeTab
|
||||
);
|
||||
render() {
|
||||
const { logs, isResult, additionalHeight, logsNow } = this.props;
|
||||
const time = logsNow.length > 0 ? logsNow[logsNow.length - 1].time : undefined;
|
||||
const { filter, activeTab, currentError } = this.state;
|
||||
const filterRE = getRE(filter, 'i');
|
||||
const filtered = logs.filter(({ level, value }) =>
|
||||
activeTab === ALL
|
||||
? filterRE.test(value)
|
||||
: filterRE.test(value) && LEVEL_TAB[level] === activeTab
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }}>
|
||||
<BottomBlock.Header showClose={!isResult}>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">Console</span>
|
||||
<Tabs tabs={TABS} active={activeTab} onClick={this.onTabClick} border={false} />
|
||||
</div>
|
||||
<Input
|
||||
className="input-small"
|
||||
placeholder="Filter by keyword"
|
||||
icon="search"
|
||||
iconPosition="left"
|
||||
name="filter"
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
<NoContent
|
||||
title={<div className="capitalize flex items-center mt-16">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
No {activeTab === ALL ? 'Data' : activeTab.toLowerCase()}</div>}
|
||||
size="small"
|
||||
show={filtered.length === 0}
|
||||
>
|
||||
<Autoscroll>
|
||||
{filtered.map((l, index) => (
|
||||
<div
|
||||
key={l.key}
|
||||
className={cn(stl.line, {
|
||||
info: !l.isYellow() && !l.isRed(),
|
||||
warn: l.isYellow(),
|
||||
error: l.isRed(),
|
||||
'cursor-pointer': !isResult,
|
||||
[stl.activeRow]: lastIndex === index,
|
||||
})}
|
||||
data-scroll-item={l.isRed()}
|
||||
onClick={() => !isResult && jump(l.time)}
|
||||
>
|
||||
<Icon size="14" className={stl.icon} {...getIconProps(l.level)} />
|
||||
<div className={stl.message}>{renderWithNL(l.value)}</div>
|
||||
</div>
|
||||
))}
|
||||
</Autoscroll>
|
||||
</NoContent>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
</>
|
||||
);
|
||||
}
|
||||
const lastIndex = filtered.filter((item) => item.time <= time).length - 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }}>
|
||||
<BottomBlock.Header showClose={!isResult}>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">Console</span>
|
||||
<Tabs tabs={TABS} active={activeTab} onClick={this.onTabClick} border={false} />
|
||||
</div>
|
||||
<Input
|
||||
className="input-small"
|
||||
placeholder="Filter by keyword"
|
||||
icon="search"
|
||||
iconPosition="left"
|
||||
name="filter"
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
<NoContent size="small" show={filtered.length === 0}>
|
||||
<Autoscroll autoScrollTo={Math.max(lastIndex, 0)}>
|
||||
{filtered.map((l, index) => (
|
||||
<div
|
||||
className={cn('flex py-2 px-4', {
|
||||
info: !l.isYellow() && !l.isRed(),
|
||||
warn: l.isYellow(),
|
||||
error: l.isRed(),
|
||||
[stl.activeRow]: lastIndex === index,
|
||||
[stl.inactiveRow]: index > lastIndex,
|
||||
'cursor-pointer': !isResult,
|
||||
})}
|
||||
onClick={() => !isResult && jump(l.time)}
|
||||
>
|
||||
<div className={cn(stl.timestamp)}>
|
||||
<Icon size="14" className={stl.icon} {...getIconProps(l.level)} />
|
||||
</div>
|
||||
<div className={cn(stl.timestamp, {})}>
|
||||
{Duration.fromMillis(l.time).toFormat('mm:ss.SSS')}
|
||||
</div>
|
||||
<div key={l.key} className={cn(stl.line)} data-scroll-item={l.isRed()}>
|
||||
<div className={stl.message}>{renderWithNL(l.value)}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Autoscroll>
|
||||
</NoContent>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,18 +11,25 @@
|
|||
|
||||
.line {
|
||||
font-family: 'Menlo', 'monaco', 'consolas', monospace;
|
||||
padding: 7px 0 7px 15px;
|
||||
/* margin-top: -1px; ??? */
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
border-bottom: solid thin $gray-light-shade;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
|
||||
}
|
||||
|
||||
.activeRow {
|
||||
background-color: $teal !important;
|
||||
color: white !important;
|
||||
background-color: $teal-light !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding-top: 4px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.inactiveRow {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
|
@ -1,117 +1,154 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { getRE } from 'App/utils';
|
||||
import { NoContent, Loader, Input, ErrorItem, SlideModal, ErrorDetails, ErrorHeader,Link, QuestionMarkHint } from 'UI';
|
||||
import { fetchErrorStackList } from 'Duck/sessions'
|
||||
import {
|
||||
NoContent,
|
||||
Loader,
|
||||
Input,
|
||||
ErrorItem,
|
||||
SlideModal,
|
||||
ErrorDetails,
|
||||
ErrorHeader,
|
||||
Link,
|
||||
QuestionMarkHint,
|
||||
Tabs,
|
||||
} from 'UI';
|
||||
import { fetchErrorStackList } from 'Duck/sessions';
|
||||
import { connectPlayer, jump } from 'Player';
|
||||
import { error as errorRoute } from 'App/routes';
|
||||
import Autoscroll from '../Autoscroll';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
|
||||
@connectPlayer(state => ({
|
||||
@connectPlayer((state) => ({
|
||||
logs: state.logListNow,
|
||||
exceptions: state.exceptionsListNow,
|
||||
exceptions: state.exceptionsList,
|
||||
exceptionsNow: state.exceptionsListNow,
|
||||
}))
|
||||
@connect(state => ({
|
||||
session: state.getIn([ 'sessions', 'current' ]),
|
||||
errorStack: state.getIn([ 'sessions', 'errorStack' ]),
|
||||
sourcemapUploaded: state.getIn([ 'sessions', 'sourcemapUploaded' ]),
|
||||
loading: state.getIn([ 'sessions', 'fetchErrorStackList', 'loading' ])
|
||||
}), { fetchErrorStackList })
|
||||
@connect(
|
||||
(state) => ({
|
||||
session: state.getIn(['sessions', 'current']),
|
||||
errorStack: state.getIn(['sessions', 'errorStack']),
|
||||
sourcemapUploaded: state.getIn(['sessions', 'sourcemapUploaded']),
|
||||
loading: state.getIn(['sessions', 'fetchErrorStackList', 'loading']),
|
||||
}),
|
||||
{ fetchErrorStackList }
|
||||
)
|
||||
export default class Exceptions extends React.PureComponent {
|
||||
state = {
|
||||
filter: '',
|
||||
currentError: null
|
||||
}
|
||||
currentError: null,
|
||||
};
|
||||
|
||||
onFilterChange = ({ target: { value } }) => this.setState({ filter: value })
|
||||
onFilterChange = ({ target: { value } }) => this.setState({ filter: value });
|
||||
|
||||
setCurrentError = (err) => {
|
||||
const { session } = this.props;
|
||||
this.props.fetchErrorStackList(session.sessionId, err.errorId)
|
||||
this.setState({ currentError: err})
|
||||
}
|
||||
closeModal = () => this.setState({ currentError: null})
|
||||
this.props.fetchErrorStackList(session.sessionId, err.errorId);
|
||||
this.setState({ currentError: err });
|
||||
};
|
||||
closeModal = () => this.setState({ currentError: null });
|
||||
|
||||
render() {
|
||||
const { exceptions, loading, errorStack, sourcemapUploaded } = this.props;
|
||||
const { filter, currentError } = this.state;
|
||||
const filterRE = getRE(filter, 'i');
|
||||
|
||||
const filtered = exceptions.filter(e => filterRE.test(e.name) || filterRE.test(e.message));
|
||||
const filtered = exceptions.filter((e) => filterRE.test(e.name) || filterRE.test(e.message));
|
||||
|
||||
let lastIndex = -1;
|
||||
filtered.forEach((item, index) => {
|
||||
if (
|
||||
this.props.exceptionsNow.length > 0 &&
|
||||
item.time <= this.props.exceptionsNow[this.props.exceptionsNow.length - 1].time
|
||||
) {
|
||||
lastIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlideModal
|
||||
title={ currentError &&
|
||||
<div className="mb-4">
|
||||
<div className="text-xl mb-2">
|
||||
<Link to={errorRoute(currentError.errorId)}>
|
||||
<span className="font-bold">{currentError.name}</span>
|
||||
</Link>
|
||||
<span className="ml-2 text-sm color-gray-medium">
|
||||
{currentError.function}
|
||||
</span>
|
||||
<SlideModal
|
||||
title={
|
||||
currentError && (
|
||||
<div className="mb-4">
|
||||
<div className="text-xl mb-2">
|
||||
<Link to={errorRoute(currentError.errorId)}>
|
||||
<span className="font-bold">{currentError.name}</span>
|
||||
</Link>
|
||||
<span className="ml-2 text-sm color-gray-medium">{currentError.function}</span>
|
||||
</div>
|
||||
<div>{currentError.message}</div>
|
||||
</div>
|
||||
<div>{currentError.message}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
isDisplayed={ currentError != null }
|
||||
content={ currentError &&
|
||||
<div className="px-4">
|
||||
<Loader loading={ loading }>
|
||||
<NoContent
|
||||
show={ !loading && errorStack.size === 0 }
|
||||
title="Nothing found!"
|
||||
>
|
||||
<ErrorDetails error={ currentError } errorStack={errorStack} sourcemapUploaded={sourcemapUploaded} />
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
isDisplayed={currentError != null}
|
||||
content={
|
||||
currentError && (
|
||||
<div className="px-4">
|
||||
<Loader loading={loading}>
|
||||
<NoContent show={!loading && errorStack.size === 0} title="Nothing found!">
|
||||
<ErrorDetails
|
||||
error={currentError}
|
||||
errorStack={errorStack}
|
||||
sourcemapUploaded={sourcemapUploaded}
|
||||
/>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
onClose={ this.closeModal }
|
||||
onClose={this.closeModal}
|
||||
/>
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
<div></div>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">Exceptions</span>
|
||||
</div>
|
||||
|
||||
<div className={'flex items-center justify-between'}>
|
||||
<Input
|
||||
// className="input-small"
|
||||
className="input-small"
|
||||
placeholder="Filter by name or message"
|
||||
icon="search"
|
||||
iconPosition="left"
|
||||
name="filter"
|
||||
onChange={ this.onFilterChange }
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
<QuestionMarkHint
|
||||
className={'ml-8'}
|
||||
content={
|
||||
<>
|
||||
<a
|
||||
className="color-teal underline"
|
||||
target="_blank"
|
||||
href="https://docs.openreplay.com/installation/upload-sourcemaps"
|
||||
>
|
||||
Upload Source Maps{' '}
|
||||
</a>
|
||||
and see source code context obtained from stack traces in their original form.
|
||||
</>
|
||||
}
|
||||
className="mr-8"
|
||||
/>
|
||||
<div className="mx-4">
|
||||
<QuestionMarkHint
|
||||
onHover={true}
|
||||
content={
|
||||
<>
|
||||
<a className="color-teal underline" target="_blank" href="https://docs.openreplay.com/installation/upload-sourcemaps">Upload Source Maps </a>
|
||||
and see source code context obtained from stack traces in their original form.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ filtered.length === 0}
|
||||
title="No recordings found"
|
||||
>
|
||||
<Autoscroll>
|
||||
{ filtered.map(e => (
|
||||
<ErrorItem
|
||||
onJump={ () => jump(e.time) }
|
||||
error={e}
|
||||
key={e.key}
|
||||
onErrorClick={() => this.setCurrentError(e)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<NoContent size="small" show={filtered.length === 0} title="No recordings found">
|
||||
<Autoscroll autoScrollTo={Math.max(lastIndex, 0)}>
|
||||
{filtered.map((e, index) => (
|
||||
<ErrorItem
|
||||
onJump={() => jump(e.time)}
|
||||
error={e}
|
||||
key={e.key}
|
||||
selected={lastIndex === index}
|
||||
inactive={index > lastIndex}
|
||||
onErrorClick={(jsEvent) => {
|
||||
jsEvent.stopPropagation();
|
||||
jsEvent.preventDefault();
|
||||
this.setCurrentError(e);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Autoscroll>
|
||||
</NoContent>
|
||||
</BottomBlock.Content>
|
||||
|
|
|
|||
|
|
@ -9,166 +9,183 @@ import FetchDetails from './FetchDetails';
|
|||
import { renderName, renderDuration } from '../Network';
|
||||
import { connect } from 'react-redux';
|
||||
import { setTimelinePointer } from 'Duck/sessions';
|
||||
import { renderStart } from 'Components/Session_/Network/NetworkContent';
|
||||
|
||||
@connectPlayer((state) => ({
|
||||
list: state.fetchList,
|
||||
listNow: state.fetchListNow,
|
||||
livePlay: state.livePlay,
|
||||
list: state.fetchList,
|
||||
listNow: state.fetchListNow,
|
||||
livePlay: state.livePlay,
|
||||
}))
|
||||
@connect(
|
||||
(state) => ({
|
||||
timelinePointer: state.getIn(['sessions', 'timelinePointer']),
|
||||
}),
|
||||
{ setTimelinePointer }
|
||||
(state) => ({
|
||||
timelinePointer: state.getIn(['sessions', 'timelinePointer']),
|
||||
}),
|
||||
{ setTimelinePointer }
|
||||
)
|
||||
export default class Fetch extends React.PureComponent {
|
||||
state = {
|
||||
filter: '',
|
||||
filteredList: this.props.list,
|
||||
current: null,
|
||||
currentIndex: 0,
|
||||
showFetchDetails: false,
|
||||
hasNextError: false,
|
||||
hasPreviousError: false,
|
||||
};
|
||||
state = {
|
||||
filter: '',
|
||||
filteredList: this.props.list,
|
||||
current: null,
|
||||
currentIndex: 0,
|
||||
showFetchDetails: false,
|
||||
hasNextError: false,
|
||||
hasPreviousError: false,
|
||||
};
|
||||
|
||||
onFilterChange = ({ target: { value } }) => {
|
||||
const { list } = this.props;
|
||||
const filterRE = getRE(value, 'i');
|
||||
const filtered = list.filter((r) => filterRE.test(r.name) || filterRE.test(r.url) || filterRE.test(r.method) || filterRE.test(r.status));
|
||||
this.setState({ filter: value, filteredList: value ? filtered : list, currentIndex: 0 });
|
||||
};
|
||||
onFilterChange = (e, { value }) => {
|
||||
const { list } = this.props;
|
||||
const filterRE = getRE(value, 'i');
|
||||
const filtered = list.filter(
|
||||
(r) =>
|
||||
filterRE.test(r.name) ||
|
||||
filterRE.test(r.url) ||
|
||||
filterRE.test(r.method) ||
|
||||
filterRE.test(r.status)
|
||||
);
|
||||
this.setState({ filter: value, filteredList: value ? filtered : list, currentIndex: 0 });
|
||||
};
|
||||
|
||||
setCurrent = (item, index) => {
|
||||
if (!this.props.livePlay) {
|
||||
pause();
|
||||
jump(item.time);
|
||||
}
|
||||
this.setState({ current: item, currentIndex: index });
|
||||
};
|
||||
|
||||
onRowClick = (item, index) => {
|
||||
if (!this.props.livePlay) {
|
||||
pause();
|
||||
}
|
||||
this.setState({ current: item, currentIndex: index, showFetchDetails: true });
|
||||
this.props.setTimelinePointer(null);
|
||||
};
|
||||
|
||||
closeModal = () => this.setState({ current: null, showFetchDetails: false });
|
||||
|
||||
nextClickHander = () => {
|
||||
// const { list } = this.props;
|
||||
const { currentIndex, filteredList } = this.state;
|
||||
|
||||
if (currentIndex === filteredList.length - 1) return;
|
||||
const newIndex = currentIndex + 1;
|
||||
this.setCurrent(filteredList[newIndex], newIndex);
|
||||
this.setState({ showFetchDetails: true });
|
||||
};
|
||||
|
||||
prevClickHander = () => {
|
||||
// const { list } = this.props;
|
||||
const { currentIndex, filteredList } = this.state;
|
||||
|
||||
if (currentIndex === 0) return;
|
||||
const newIndex = currentIndex - 1;
|
||||
this.setCurrent(filteredList[newIndex], newIndex);
|
||||
this.setState({ showFetchDetails: true });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { listNow } = this.props;
|
||||
const { current, currentIndex, showFetchDetails, filteredList } = this.state;
|
||||
const hasErrors = filteredList.some((r) => r.status >= 400);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<SlideModal
|
||||
right
|
||||
size="middle"
|
||||
title={
|
||||
<div className="flex justify-between">
|
||||
<h1>Fetch Request</h1>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 color-gray-medium uppercase text-base">Status</span>
|
||||
<Label data-red={current && current.status >= 400} data-green={current && current.status < 400}>
|
||||
<div className="uppercase w-16 justify-center code-font text-lg">{current && current.status}</div>
|
||||
</Label>
|
||||
</div>
|
||||
<CloseButton onClick={this.closeModal} size="18" className="ml-2" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
isDisplayed={current != null && showFetchDetails}
|
||||
content={
|
||||
current &&
|
||||
showFetchDetails && (
|
||||
<FetchDetails
|
||||
resource={current}
|
||||
nextClick={this.nextClickHander}
|
||||
prevClick={this.prevClickHander}
|
||||
first={currentIndex === 0}
|
||||
last={currentIndex === filteredList.length - 1}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClose={this.closeModal}
|
||||
/>
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
<span className="font-semibold color-gray-medium mr-4">Fetch</span>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
className="input-small"
|
||||
placeholder="Filter"
|
||||
icon="search"
|
||||
iconPosition="left"
|
||||
name="filter"
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
<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}
|
||||
>
|
||||
{/* <NoContent size="small" show={filteredList.length === 0}> */}
|
||||
<TimeTable rows={filteredList} onRowClick={this.onRowClick} hoverable navigation={hasErrors} activeIndex={listNow.length - 1}>
|
||||
{[
|
||||
{
|
||||
label: 'Status',
|
||||
dataKey: 'status',
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
label: 'Method',
|
||||
dataKey: 'method',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
label: 'Name',
|
||||
width: 240,
|
||||
render: renderName,
|
||||
},
|
||||
{
|
||||
label: 'Time',
|
||||
width: 80,
|
||||
render: renderDuration,
|
||||
},
|
||||
]}
|
||||
</TimeTable>
|
||||
</NoContent>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
</React.Fragment>
|
||||
);
|
||||
setCurrent = (item, index) => {
|
||||
if (!this.props.livePlay) {
|
||||
pause();
|
||||
jump(item.time);
|
||||
}
|
||||
this.setState({ current: item, currentIndex: index });
|
||||
};
|
||||
|
||||
onRowClick = (item, index) => {
|
||||
if (!this.props.livePlay) {
|
||||
pause();
|
||||
}
|
||||
this.setState({ current: item, currentIndex: index, showFetchDetails: true });
|
||||
this.props.setTimelinePointer(null);
|
||||
};
|
||||
|
||||
closeModal = () => this.setState({ current: null, showFetchDetails: false });
|
||||
|
||||
nextClickHander = () => {
|
||||
// const { list } = this.props;
|
||||
const { currentIndex, filteredList } = this.state;
|
||||
|
||||
if (currentIndex === filteredList.length - 1) return;
|
||||
const newIndex = currentIndex + 1;
|
||||
this.setCurrent(filteredList[newIndex], newIndex);
|
||||
this.setState({ showFetchDetails: true });
|
||||
};
|
||||
|
||||
prevClickHander = () => {
|
||||
// const { list } = this.props;
|
||||
const { currentIndex, filteredList } = this.state;
|
||||
|
||||
if (currentIndex === 0) return;
|
||||
const newIndex = currentIndex - 1;
|
||||
this.setCurrent(filteredList[newIndex], newIndex);
|
||||
this.setState({ showFetchDetails: true });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { listNow } = this.props;
|
||||
const { current, currentIndex, showFetchDetails, filteredList } = this.state;
|
||||
const hasErrors = filteredList.some((r) => r.status >= 400);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<SlideModal
|
||||
right
|
||||
size="middle"
|
||||
title={
|
||||
<div className="flex justify-between">
|
||||
<h1>Fetch Request</h1>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 color-gray-medium uppercase text-base">Status</span>
|
||||
<Label
|
||||
data-red={current && current.status >= 400}
|
||||
data-green={current && current.status < 400}
|
||||
>
|
||||
<div className="uppercase w-16 justify-center code-font text-lg">
|
||||
{current && current.status}
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
<CloseButton onClick={this.closeModal} size="18" className="ml-2" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
isDisplayed={current != null && showFetchDetails}
|
||||
content={
|
||||
current &&
|
||||
showFetchDetails && (
|
||||
<FetchDetails
|
||||
resource={current}
|
||||
nextClick={this.nextClickHander}
|
||||
prevClick={this.prevClickHander}
|
||||
first={currentIndex === 0}
|
||||
last={currentIndex === filteredList.length - 1}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClose={this.closeModal}
|
||||
/>
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
<span className="font-semibold color-gray-medium mr-4">Fetch</span>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
className="input-small"
|
||||
placeholder="Filter"
|
||||
icon="search"
|
||||
iconPosition="left"
|
||||
name="filter"
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
<NoContent title={
|
||||
<div className="capitalize flex items-center mt-16">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
No Data
|
||||
</div>
|
||||
} show={filteredList.length === 0}>
|
||||
<TimeTable
|
||||
rows={filteredList}
|
||||
onRowClick={this.onRowClick}
|
||||
hoverable
|
||||
activeIndex={listNow.length - 1}
|
||||
>
|
||||
{[
|
||||
{
|
||||
label: 'Start',
|
||||
width: 90,
|
||||
render: renderStart,
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
dataKey: 'status',
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
label: 'Method',
|
||||
dataKey: 'method',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
label: 'Name',
|
||||
width: 240,
|
||||
render: renderName,
|
||||
},
|
||||
{
|
||||
label: 'Time',
|
||||
width: 80,
|
||||
render: renderDuration,
|
||||
},
|
||||
]}
|
||||
</TimeTable>
|
||||
</NoContent>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,80 +1,72 @@
|
|||
import React from 'react';
|
||||
import { JSONTree, Button } from 'UI'
|
||||
import { JSONTree, Button } from 'UI';
|
||||
import cn from 'classnames';
|
||||
|
||||
export default class GQLDetails extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
gql: {
|
||||
variables,
|
||||
response,
|
||||
duration,
|
||||
operationKind,
|
||||
operationName,
|
||||
},
|
||||
nextClick,
|
||||
prevClick,
|
||||
first = false,
|
||||
last = false,
|
||||
} = this.props;
|
||||
render() {
|
||||
const {
|
||||
gql: { variables, response, duration, operationKind, operationName },
|
||||
nextClick,
|
||||
prevClick,
|
||||
first = false,
|
||||
last = false,
|
||||
} = this.props;
|
||||
|
||||
let jsonVars = undefined;
|
||||
let jsonResponse = undefined;
|
||||
try {
|
||||
jsonVars = JSON.parse(payload);
|
||||
} catch (e) {}
|
||||
try {
|
||||
jsonResponse = JSON.parse(response);
|
||||
} catch (e) {}
|
||||
return (
|
||||
<div className="px-4 pb-16">
|
||||
<h5 className="mb-2">{ 'Operation Name'}</h5>
|
||||
<div className={ cn('p-2 bg-gray-lightest rounded color-gray-darkest')}>{ operationName }</div>
|
||||
let jsonVars = undefined;
|
||||
let jsonResponse = undefined;
|
||||
try {
|
||||
jsonVars = JSON.parse(variables);
|
||||
} catch (e) {}
|
||||
try {
|
||||
jsonResponse = JSON.parse(response);
|
||||
} catch (e) {}
|
||||
const dataClass = cn('p-2 bg-gray-lightest rounded color-gray-darkest');
|
||||
return (
|
||||
<div className="px-4 pb-16">
|
||||
<h5 className="mb-2">{'Operation Name'}</h5>
|
||||
<div className={dataClass}>{operationName}</div>
|
||||
|
||||
<div className="flex items-center mt-4">
|
||||
<div className="w-4/12">
|
||||
<div className="font-medium mb-2">Operation Kind</div>
|
||||
<div>{operationKind}</div>
|
||||
</div>
|
||||
<div className="w-4/12">
|
||||
<div className="font-medium mb-2">Duration</div>
|
||||
<div>{parseInt(duration)} ms</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-4">
|
||||
<div className="w-6/12">
|
||||
<div className="mb-2">Operation Kind</div>
|
||||
<div className={dataClass}>{operationKind}</div>
|
||||
</div>
|
||||
<div className="w-6/12">
|
||||
<div className="mb-2">Duration</div>
|
||||
<div className={dataClass}>{duration ? parseInt(duration) : '???'} ms</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-start mt-6">
|
||||
<h5 className="mt-1 mr-1">{ 'Response' }</h5>
|
||||
</div>
|
||||
<div style={{ height: 'calc(100vh - 314px)', overflowY: 'auto' }}>
|
||||
{ variables && variables !== "{}" &&
|
||||
<div>
|
||||
<div className="mt-2">
|
||||
<h5>{ 'Variables'}</h5>
|
||||
{ jsonVars === undefined
|
||||
? <div className="ml-3"> { variables } </div>
|
||||
: <JSONTree src={ jsonVars } />
|
||||
}
|
||||
</div>
|
||||
<div className="divider"/>
|
||||
</div>
|
||||
}
|
||||
<div className="mt-3">
|
||||
{ jsonResponse === undefined
|
||||
? <div className="ml-3"> { response } </div>
|
||||
: <JSONTree src={ jsonResponse } />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ height: 'calc(100vh - 314px)', overflowY: 'auto' }}>
|
||||
<div>
|
||||
<div className="flex justify-between items-start mt-6 mb-2">
|
||||
<h5 className="mt-1 mr-1">{'Variables'}</h5>
|
||||
</div>
|
||||
<div className={dataClass}>
|
||||
{jsonVars === undefined ? variables : <JSONTree src={jsonVars} />}
|
||||
</div>
|
||||
<div className="divider" />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between absolute bottom-0 left-0 right-0 p-3 border-t bg-white">
|
||||
<Button variant="outline" onClick={prevClick} disabled={first}>
|
||||
Prev
|
||||
</Button>
|
||||
<Button variant="outline" onClick={nextClick} disabled={last}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
<div>
|
||||
<div className="flex justify-between items-start mt-6 mb-2">
|
||||
<h5 className="mt-1 mr-1">{'Response'}</h5>
|
||||
</div>
|
||||
<div className={dataClass}>
|
||||
{jsonResponse === undefined ? response : <JSONTree src={jsonResponse} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between absolute bottom-0 left-0 right-0 p-3 border-t bg-white">
|
||||
<Button variant="outline" onClick={prevClick} disabled={first}>
|
||||
Prev
|
||||
</Button>
|
||||
<Button variant="outline" onClick={nextClick} disabled={last}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,87 +1,134 @@
|
|||
import React from 'react';
|
||||
import { NoContent, Input, SlideModal, CloseButton } from 'UI';
|
||||
import { NoContent, Input, SlideModal, CloseButton, Button } from 'UI';
|
||||
import { getRE } from 'App/utils';
|
||||
import { connectPlayer, pause, jump } from 'Player';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import TimeTable from '../TimeTable';
|
||||
import GQLDetails from './GQLDetails';
|
||||
import { renderStart } from 'Components/Session_/Network/NetworkContent';
|
||||
|
||||
function renderDefaultStatus() {
|
||||
return "2xx-3xx";
|
||||
return '2xx-3xx';
|
||||
}
|
||||
@connectPlayer(state => ({
|
||||
list: state.graphqlListNow,
|
||||
|
||||
export function renderName(r) {
|
||||
return (
|
||||
<div className="flex justify-between items-center grow-0 w-full">
|
||||
<div>{r.operationName}</div>
|
||||
<Button
|
||||
variant="text"
|
||||
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
jump(r.time);
|
||||
}}
|
||||
>
|
||||
Jump
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@connectPlayer((state) => ({
|
||||
list: state.graphqlList,
|
||||
listNow: state.graphqlListNow,
|
||||
time: state.time,
|
||||
livePlay: state.livePlay,
|
||||
}))
|
||||
export default class GraphQL extends React.PureComponent {
|
||||
state = {
|
||||
filter: "",
|
||||
state = {
|
||||
filter: '',
|
||||
filteredList: this.props.list,
|
||||
current: null,
|
||||
filteredListNow: this.props.listNow,
|
||||
current: null,
|
||||
currentIndex: 0,
|
||||
showFetchDetails: false,
|
||||
hasNextError: false,
|
||||
hasPreviousError: false,
|
||||
}
|
||||
lastActiveItem: 0,
|
||||
};
|
||||
|
||||
static filterList(list, value) {
|
||||
const filterRE = getRE(value, 'i');
|
||||
|
||||
return value
|
||||
? list.filter(
|
||||
(r) =>
|
||||
filterRE.test(r.operationKind) ||
|
||||
filterRE.test(r.operationName) ||
|
||||
filterRE.test(r.variables)
|
||||
)
|
||||
: list;
|
||||
}
|
||||
|
||||
onFilterChange = ({ target: { value } }) => {
|
||||
const { list } = this.props;
|
||||
const filterRE = getRE(value, 'i');
|
||||
const filtered = list
|
||||
.filter((r) => filterRE.test(r.name) || filterRE.test(r.url) || filterRE.test(r.method) || filterRE.test(r.status));
|
||||
this.setState({ filter: value, filteredList: value ? filtered : list, currentIndex: 0 });
|
||||
}
|
||||
const filtered = GraphQL.filterList(list, value);
|
||||
this.setState({ filter: value, filteredList: filtered, currentIndex: 0 });
|
||||
};
|
||||
|
||||
setCurrent = (item, index) => {
|
||||
if (!this.props.livePlay) {
|
||||
pause();
|
||||
jump(item.time)
|
||||
jump(item.time);
|
||||
}
|
||||
this.setState({ current: item, currentIndex: index });
|
||||
}
|
||||
};
|
||||
|
||||
closeModal = () => this.setState({ current: null, showFetchDetails: false });
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
const { filteredList } = prevState;
|
||||
if (nextProps.timelinePointer) {
|
||||
let activeItem = filteredList.find((r) => r.time >= nextProps.timelinePointer.time);
|
||||
activeItem = activeItem || filteredList[filteredList.length - 1];
|
||||
const { list } = nextProps;
|
||||
if (nextProps.time) {
|
||||
const filtered = GraphQL.filterList(list, prevState.filter);
|
||||
console.log({
|
||||
list,
|
||||
filtered,
|
||||
time: nextProps.time,
|
||||
});
|
||||
|
||||
let i = 0;
|
||||
filtered.forEach((item, index) => {
|
||||
if (item.time <= nextProps.time) {
|
||||
i = index;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
current: activeItem,
|
||||
currentIndex: filteredList.indexOf(activeItem),
|
||||
lastActiveItem: i,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { list } = this.props;
|
||||
const { current, currentIndex, filteredList } = this.state;
|
||||
|
||||
const { current, currentIndex, filteredList, lastActiveItem } = this.state;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<SlideModal
|
||||
<SlideModal
|
||||
size="middle"
|
||||
right
|
||||
title = {
|
||||
title={
|
||||
<div className="flex justify-between">
|
||||
<h1>GraphQL</h1>
|
||||
<div className="flex items-center">
|
||||
<CloseButton onClick={ this.closeModal } size="18" className="ml-2" />
|
||||
<CloseButton onClick={this.closeModal} size="18" className="ml-2" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
isDisplayed={ current != null }
|
||||
content={ current &&
|
||||
<GQLDetails
|
||||
gql={ current }
|
||||
nextClick={this.nextClickHander}
|
||||
prevClick={this.prevClickHander}
|
||||
first={currentIndex === 0}
|
||||
last={currentIndex === filteredList.length - 1}
|
||||
/>
|
||||
isDisplayed={current != null}
|
||||
content={
|
||||
current && (
|
||||
<GQLDetails
|
||||
gql={current}
|
||||
nextClick={this.nextClickHander}
|
||||
prevClick={this.prevClickHander}
|
||||
first={currentIndex === 0}
|
||||
last={currentIndex === filteredList.length - 1}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClose={ this.closeModal }
|
||||
onClose={this.closeModal}
|
||||
/>
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
|
|
@ -93,36 +140,38 @@ export default class GraphQL extends React.PureComponent {
|
|||
icon="search"
|
||||
iconPosition="left"
|
||||
name="filter"
|
||||
onChange={ this.onFilterChange }
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ filteredList.length === 0}
|
||||
>
|
||||
<NoContent size="small" title="No recordings found" show={filteredList.length === 0}>
|
||||
<TimeTable
|
||||
rows={ filteredList }
|
||||
onRowClick={ this.setCurrent }
|
||||
rows={filteredList}
|
||||
onRowClick={this.setCurrent}
|
||||
hoverable
|
||||
navigation
|
||||
activeIndex={currentIndex}
|
||||
activeIndex={lastActiveItem}
|
||||
>
|
||||
{[
|
||||
{
|
||||
label: "Status",
|
||||
label: 'Start',
|
||||
width: 90,
|
||||
render: renderStart,
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
width: 70,
|
||||
render: renderDefaultStatus,
|
||||
}, {
|
||||
label: "Type",
|
||||
dataKey: "operationKind",
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
dataKey: 'operationKind',
|
||||
width: 60,
|
||||
}, {
|
||||
label: "Name",
|
||||
width: 130,
|
||||
dataKey: "operationName",
|
||||
},
|
||||
{
|
||||
label: 'Name',
|
||||
width: 240,
|
||||
render: renderName,
|
||||
},
|
||||
]}
|
||||
</TimeTable>
|
||||
|
|
|
|||
|
|
@ -18,128 +18,141 @@ const MEDIA = 'media';
|
|||
const OTHER = 'other';
|
||||
|
||||
const TAB_TO_TYPE_MAP = {
|
||||
[XHR]: TYPES.XHR,
|
||||
[JS]: TYPES.JS,
|
||||
[CSS]: TYPES.CSS,
|
||||
[IMG]: TYPES.IMG,
|
||||
[MEDIA]: TYPES.MEDIA,
|
||||
[OTHER]: TYPES.OTHER,
|
||||
[XHR]: TYPES.XHR,
|
||||
[JS]: TYPES.JS,
|
||||
[CSS]: TYPES.CSS,
|
||||
[IMG]: TYPES.IMG,
|
||||
[MEDIA]: TYPES.MEDIA,
|
||||
[OTHER]: TYPES.OTHER,
|
||||
};
|
||||
|
||||
export function renderName(r) {
|
||||
return (
|
||||
<div className="flex justify-between items-center grow-0 w-full">
|
||||
<Popup style={{ maxWidth: '75%' }} content={<div className={stl.popupNameContent}>{r.url}</div>}>
|
||||
<TextEllipsis>{r.name}</TextEllipsis>
|
||||
</Popup>
|
||||
<Button
|
||||
variant="text"
|
||||
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
jump(r.time);
|
||||
}}
|
||||
>
|
||||
Jump
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex justify-between items-center grow-0 w-full">
|
||||
<Popup
|
||||
style={{ maxWidth: '75%' }}
|
||||
content={<div className={stl.popupNameContent}>{r.url}</div>}
|
||||
>
|
||||
<TextEllipsis>{r.name}</TextEllipsis>
|
||||
</Popup>
|
||||
<Button
|
||||
variant="text"
|
||||
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
jump(r.time);
|
||||
}}
|
||||
>
|
||||
Jump
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderDuration(r) {
|
||||
if (!r.success) return 'x';
|
||||
if (!r.success) return 'x';
|
||||
|
||||
const text = `${Math.round(r.duration)}ms`;
|
||||
if (!r.isRed() && !r.isYellow()) return text;
|
||||
const text = `${Math.round(r.duration)}ms`;
|
||||
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';
|
||||
}
|
||||
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 (
|
||||
<Popup content={tooltipText}>
|
||||
<div className={cn(className, stl.duration)}> {text} </div>
|
||||
</Popup>
|
||||
);
|
||||
return (
|
||||
<Popup content={tooltipText}>
|
||||
<div className={cn(className, stl.duration)}> {text} </div>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
@connectPlayer((state) => ({
|
||||
location: state.location,
|
||||
resources: state.resourceList,
|
||||
domContentLoadedTime: state.domContentLoadedTime,
|
||||
loadTime: state.loadTime,
|
||||
// time: state.time,
|
||||
playing: state.playing,
|
||||
domBuildingTime: state.domBuildingTime,
|
||||
fetchPresented: state.fetchList.length > 0,
|
||||
listNow: state.resourceListNow,
|
||||
location: state.location,
|
||||
resources: state.resourceList,
|
||||
domContentLoadedTime: state.domContentLoadedTime,
|
||||
loadTime: state.loadTime,
|
||||
// time: state.time,
|
||||
playing: state.playing,
|
||||
domBuildingTime: state.domBuildingTime,
|
||||
fetchPresented: state.fetchList.length > 0,
|
||||
listNow: state.resourceListNow,
|
||||
}))
|
||||
@connect(
|
||||
(state) => ({
|
||||
timelinePointer: state.getIn(['sessions', 'timelinePointer']),
|
||||
}),
|
||||
{ setTimelinePointer }
|
||||
(state) => ({
|
||||
timelinePointer: state.getIn(['sessions', 'timelinePointer']),
|
||||
}),
|
||||
{ setTimelinePointer }
|
||||
)
|
||||
export default class Network extends React.PureComponent {
|
||||
state = {
|
||||
filter: '',
|
||||
filteredList: this.props.resources,
|
||||
activeTab: ALL,
|
||||
currentIndex: 0,
|
||||
};
|
||||
state = {
|
||||
filter: '',
|
||||
filteredList: this.props.resources,
|
||||
activeTab: ALL,
|
||||
currentIndex: 0,
|
||||
};
|
||||
|
||||
onRowClick = (e, index) => {
|
||||
pause();
|
||||
jump(e.time);
|
||||
this.setState({ currentIndex: index });
|
||||
this.props.setTimelinePointer(null);
|
||||
};
|
||||
onRowClick = (e, index) => {
|
||||
// no action for direct click on network requests (so far), there is a jump button, and we don't have more information for than is already displayed in the table
|
||||
};
|
||||
|
||||
onTabClick = (activeTab) => this.setState({ activeTab });
|
||||
onTabClick = (activeTab) => this.setState({ activeTab });
|
||||
|
||||
onFilterChange = (e, { value }) => {
|
||||
const { resources } = this.props;
|
||||
const filterRE = getRE(value, 'i');
|
||||
const filtered = resources.filter(({ type, name }) => filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab]));
|
||||
onFilterChange = (e, { value }) => {
|
||||
const { resources } = this.props;
|
||||
const filterRE = getRE(value, 'i');
|
||||
const filtered = resources.filter(
|
||||
({ type, name }) =>
|
||||
filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab])
|
||||
);
|
||||
|
||||
this.setState({ filter: value, filteredList: value ? filtered : resources, currentIndex: 0 });
|
||||
};
|
||||
this.setState({ filter: value, filteredList: value ? filtered : resources, currentIndex: 0 });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
location,
|
||||
domContentLoadedTime,
|
||||
loadTime,
|
||||
domBuildingTime,
|
||||
fetchPresented,
|
||||
listNow,
|
||||
} = this.props;
|
||||
const { filteredList } = this.state;
|
||||
const resourcesSize = filteredList.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0);
|
||||
const transferredSize = filteredList.reduce((sum, { headerSize, encodedBodySize }) => sum + (headerSize || 0) + (encodedBodySize || 0), 0);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NetworkContent
|
||||
// time = { time }
|
||||
location={location}
|
||||
resources={filteredList}
|
||||
domContentLoadedTime={domContentLoadedTime}
|
||||
loadTime={loadTime}
|
||||
domBuildingTime={domBuildingTime}
|
||||
fetchPresented={fetchPresented}
|
||||
resourcesSize={resourcesSize}
|
||||
transferredSize={transferredSize}
|
||||
onRowClick={this.onRowClick}
|
||||
currentIndex={listNow.length - 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
const { filteredList } = prevState;
|
||||
if (nextProps.timelinePointer) {
|
||||
const activeItem = filteredList.find((r) => r.time >= nextProps.timelinePointer.time);
|
||||
return {
|
||||
currentIndex: activeItem ? filteredList.indexOf(activeItem) : filteredList.length - 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { location, domContentLoadedTime, loadTime, domBuildingTime, fetchPresented, listNow } =
|
||||
this.props;
|
||||
const { filteredList } = this.state;
|
||||
const resourcesSize = filteredList.reduce(
|
||||
(sum, { decodedBodySize }) => sum + (decodedBodySize || 0),
|
||||
0
|
||||
);
|
||||
const transferredSize = filteredList.reduce(
|
||||
(sum, { headerSize, encodedBodySize }) => sum + (headerSize || 0) + (encodedBodySize || 0),
|
||||
0
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NetworkContent
|
||||
// time = { time }
|
||||
location={location}
|
||||
resources={filteredList}
|
||||
domContentLoadedTime={domContentLoadedTime}
|
||||
loadTime={loadTime}
|
||||
domBuildingTime={domBuildingTime}
|
||||
fetchPresented={fetchPresented}
|
||||
resourcesSize={resourcesSize}
|
||||
transferredSize={transferredSize}
|
||||
onRowClick={this.onRowClick}
|
||||
currentIndex={listNow.length - 1}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
// import { connectPlayer } from 'Player';
|
||||
import { QuestionMarkHint, Popup, Tabs, Input, NoContent, Icon } from 'UI';
|
||||
import { QuestionMarkHint, Popup, Tabs, Input, NoContent, Icon, Button } from 'UI';
|
||||
import { getRE } from 'App/utils';
|
||||
import { TYPES } from 'Types/session/resource';
|
||||
import { formatBytes } from 'App/utils';
|
||||
|
|
@ -11,6 +11,8 @@ import TimeTable from '../TimeTable';
|
|||
import BottomBlock from '../BottomBlock';
|
||||
import InfoLine from '../BottomBlock/InfoLine';
|
||||
import stl from './network.module.css';
|
||||
import { Duration } from 'luxon';
|
||||
import { jump } from 'Player';
|
||||
|
||||
const ALL = 'ALL';
|
||||
const XHR = 'xhr';
|
||||
|
|
@ -21,261 +23,309 @@ const MEDIA = 'media';
|
|||
const OTHER = 'other';
|
||||
|
||||
const TAB_TO_TYPE_MAP = {
|
||||
[XHR]: TYPES.XHR,
|
||||
[JS]: TYPES.JS,
|
||||
[CSS]: TYPES.CSS,
|
||||
[IMG]: TYPES.IMG,
|
||||
[MEDIA]: TYPES.MEDIA,
|
||||
[OTHER]: TYPES.OTHER,
|
||||
[XHR]: TYPES.XHR,
|
||||
[JS]: TYPES.JS,
|
||||
[CSS]: TYPES.CSS,
|
||||
[IMG]: TYPES.IMG,
|
||||
[MEDIA]: TYPES.MEDIA,
|
||||
[OTHER]: TYPES.OTHER,
|
||||
};
|
||||
const TABS = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER].map((tab) => ({
|
||||
text: tab,
|
||||
key: tab,
|
||||
text: tab,
|
||||
key: tab,
|
||||
}));
|
||||
|
||||
const DOM_LOADED_TIME_COLOR = 'teal';
|
||||
const LOAD_TIME_COLOR = 'red';
|
||||
|
||||
export function renderType(r) {
|
||||
return (
|
||||
<Popup style={{ width: '100%' }} content={<div className={stl.popupNameContent}>{r.type}</div>}>
|
||||
<div className={stl.popupNameTrigger}>{r.type}</div>
|
||||
</Popup>
|
||||
);
|
||||
return (
|
||||
<Popup style={{ width: '100%' }} content={<div className={stl.popupNameContent}>{r.type}</div>}>
|
||||
<div className={stl.popupNameTrigger}>{r.type}</div>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderName(r) {
|
||||
return (
|
||||
<Popup style={{ width: '100%' }} content={<div className={stl.popupNameContent}>{r.url}</div>}>
|
||||
<div className={stl.popupNameTrigger}>{r.name}</div>
|
||||
</Popup>
|
||||
);
|
||||
return (
|
||||
<div className="flex justify-between items-center grow-0 w-full">
|
||||
<Popup
|
||||
style={{ width: '100%' }}
|
||||
content={<div className={stl.popupNameContent}>{r.url}</div>}
|
||||
>
|
||||
<div className={stl.popupNameTrigger}>{r.name}</div>
|
||||
</Popup>
|
||||
<Button
|
||||
variant="text"
|
||||
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
jump(r.time);
|
||||
}}
|
||||
>
|
||||
Jump
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderStart(r) {
|
||||
return Duration.fromMillis(r.time).toFormat('mm:ss.SSS');
|
||||
}
|
||||
|
||||
const renderXHRText = () => (
|
||||
<span className="flex items-center">
|
||||
{XHR}
|
||||
<QuestionMarkHint
|
||||
onHover={true}
|
||||
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>
|
||||
<span className="flex items-center">
|
||||
{XHR}
|
||||
<QuestionMarkHint
|
||||
onHover={true}
|
||||
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) {
|
||||
if (r.responseBodySize) return formatBytes(r.responseBodySize);
|
||||
let triggerText;
|
||||
let content;
|
||||
if (r.decodedBodySize == null) {
|
||||
triggerText = 'x';
|
||||
content = 'Not captured';
|
||||
} else {
|
||||
const headerSize = r.headerSize || 0;
|
||||
const encodedSize = r.encodedBodySize || 0;
|
||||
const transferred = headerSize + encodedSize;
|
||||
const showTransferred = r.headerSize != null;
|
||||
if (r.responseBodySize) return formatBytes(r.responseBodySize);
|
||||
let triggerText;
|
||||
let content;
|
||||
if (r.decodedBodySize == null) {
|
||||
triggerText = 'x';
|
||||
content = 'Not captured';
|
||||
} else {
|
||||
const headerSize = r.headerSize || 0;
|
||||
const encodedSize = r.encodedBodySize || 0;
|
||||
const transferred = headerSize + encodedSize;
|
||||
const showTransferred = r.headerSize != null;
|
||||
|
||||
triggerText = formatBytes(r.decodedBodySize);
|
||||
content = (
|
||||
<ul>
|
||||
{showTransferred && <li>{`${formatBytes(r.encodedBodySize + headerSize)} transfered over network`}</li>}
|
||||
<li>{`Resource size: ${formatBytes(r.decodedBodySize)} `}</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popup style={{ width: '100%' }} content={content}>
|
||||
<div>{triggerText}</div>
|
||||
</Popup>
|
||||
triggerText = formatBytes(r.decodedBodySize);
|
||||
content = (
|
||||
<ul>
|
||||
{showTransferred && (
|
||||
<li>{`${formatBytes(r.encodedBodySize + headerSize)} transfered over network`}</li>
|
||||
)}
|
||||
<li>{`Resource size: ${formatBytes(r.decodedBodySize)} `}</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popup style={{ width: '100%' }} content={content}>
|
||||
<div>{triggerText}</div>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderDuration(r) {
|
||||
if (!r.success) return 'x';
|
||||
if (!r.success) return 'x';
|
||||
|
||||
const text = `${Math.floor(r.duration)}ms`;
|
||||
if (!r.isRed() && !r.isYellow()) return text;
|
||||
const text = `${Math.floor(r.duration)}ms`;
|
||||
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';
|
||||
}
|
||||
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 (
|
||||
<Popup style={{ width: '100%' }} content={tooltipText}>
|
||||
<div className={cn(className, stl.duration)}> {text} </div>
|
||||
</Popup>
|
||||
);
|
||||
return (
|
||||
<Popup style={{ width: '100%' }} content={tooltipText}>
|
||||
<div className={cn(className, stl.duration)}> {text} </div>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
export default class NetworkContent extends React.PureComponent {
|
||||
state = {
|
||||
filter: '',
|
||||
activeTab: ALL,
|
||||
};
|
||||
state = {
|
||||
filter: '',
|
||||
activeTab: ALL,
|
||||
};
|
||||
|
||||
onTabClick = (activeTab) => this.setState({ activeTab });
|
||||
onFilterChange = ({ target: { value } }) => this.setState({ filter: value });
|
||||
onTabClick = (activeTab) => this.setState({ activeTab });
|
||||
onFilterChange = ({ target: { value } }) => this.setState({ filter: value });
|
||||
|
||||
render() {
|
||||
const {
|
||||
location,
|
||||
resources,
|
||||
domContentLoadedTime,
|
||||
loadTime,
|
||||
domBuildingTime,
|
||||
fetchPresented,
|
||||
onRowClick,
|
||||
isResult = false,
|
||||
additionalHeight = 0,
|
||||
resourcesSize,
|
||||
transferredSize,
|
||||
time,
|
||||
currentIndex,
|
||||
} = this.props;
|
||||
const { filter, activeTab } = this.state;
|
||||
const filterRE = getRE(filter, 'i');
|
||||
let filtered = resources.filter(({ type, name }) => filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab]));
|
||||
const lastIndex = currentIndex || filtered.filter((item) => item.time <= time).length - 1;
|
||||
render() {
|
||||
const {
|
||||
location,
|
||||
resources,
|
||||
domContentLoadedTime,
|
||||
loadTime,
|
||||
domBuildingTime,
|
||||
fetchPresented,
|
||||
onRowClick,
|
||||
isResult = false,
|
||||
additionalHeight = 0,
|
||||
resourcesSize,
|
||||
transferredSize,
|
||||
time,
|
||||
currentIndex,
|
||||
} = this.props;
|
||||
const { filter, activeTab } = this.state;
|
||||
const filterRE = getRE(filter, 'i');
|
||||
let filtered = resources.filter(
|
||||
({ type, name }) =>
|
||||
filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab])
|
||||
);
|
||||
const lastIndex = currentIndex || filtered.filter((item) => 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,
|
||||
});
|
||||
}
|
||||
|
||||
let tabs = TABS;
|
||||
if (!fetchPresented) {
|
||||
tabs = TABS.map((tab) =>
|
||||
!isResult && tab.key === XHR
|
||||
? {
|
||||
text: renderXHRText(),
|
||||
key: XHR,
|
||||
}
|
||||
: tab
|
||||
);
|
||||
}
|
||||
|
||||
// const resourcesSize = filtered.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0);
|
||||
// const transferredSize = filtered
|
||||
// .reduce((sum, { headerSize, encodedBodySize }) => sum + (headerSize || 0) + (encodedBodySize || 0), 0);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }} className="border">
|
||||
<BottomBlock.Header showClose={!isResult}>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">Network</span>
|
||||
<Tabs className="uppercase" tabs={tabs} active={activeTab} onClick={this.onTabClick} border={false} />
|
||||
</div>
|
||||
<Input
|
||||
// className="input-small"
|
||||
placeholder="Filter by Name"
|
||||
icon="search"
|
||||
iconPosition="left"
|
||||
name="filter"
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
<InfoLine>
|
||||
<InfoLine.Point label={filtered.length} value=" requests" />
|
||||
<InfoLine.Point label={formatBytes(transferredSize)} value="transferred" display={transferredSize > 0} />
|
||||
<InfoLine.Point label={formatBytes(resourcesSize)} value="resources" display={resourcesSize > 0} />
|
||||
<InfoLine.Point label="DOM Building Time" value={formatMs(domBuildingTime)} display={domBuildingTime != null} />
|
||||
<InfoLine.Point
|
||||
label="DOMContentLoaded"
|
||||
value={domContentLoadedTime && formatMs(domContentLoadedTime.value)}
|
||||
display={domContentLoadedTime != null}
|
||||
dotColor={DOM_LOADED_TIME_COLOR}
|
||||
/>
|
||||
<InfoLine.Point
|
||||
label="Load"
|
||||
value={loadTime && formatMs(loadTime.value)}
|
||||
display={loadTime != null}
|
||||
dotColor={LOAD_TIME_COLOR}
|
||||
/>
|
||||
</InfoLine>
|
||||
<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={filtered.length === 0}
|
||||
>
|
||||
<TimeTable
|
||||
rows={filtered}
|
||||
referenceLines={referenceLines}
|
||||
renderPopup
|
||||
// navigation
|
||||
onRowClick={onRowClick}
|
||||
additionalHeight={additionalHeight}
|
||||
activeIndex={lastIndex}
|
||||
>
|
||||
{[
|
||||
{
|
||||
label: 'Status',
|
||||
dataKey: 'status',
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
dataKey: 'type',
|
||||
width: 90,
|
||||
render: renderType,
|
||||
},
|
||||
{
|
||||
label: 'Name',
|
||||
width: 200,
|
||||
render: renderName,
|
||||
},
|
||||
{
|
||||
label: 'Size',
|
||||
width: 60,
|
||||
render: renderSize,
|
||||
},
|
||||
{
|
||||
label: 'Time',
|
||||
width: 80,
|
||||
render: renderDuration,
|
||||
},
|
||||
]}
|
||||
</TimeTable>
|
||||
</NoContent>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
</React.Fragment>
|
||||
);
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
let tabs = TABS;
|
||||
if (!fetchPresented) {
|
||||
tabs = TABS.map((tab) =>
|
||||
!isResult && tab.key === XHR
|
||||
? {
|
||||
text: renderXHRText(),
|
||||
key: XHR,
|
||||
}
|
||||
: tab
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }} className="border">
|
||||
<BottomBlock.Header showClose={!isResult}>
|
||||
<Tabs
|
||||
className="uppercase"
|
||||
tabs={tabs}
|
||||
active={activeTab}
|
||||
onClick={this.onTabClick}
|
||||
border={false}
|
||||
/>
|
||||
<Input
|
||||
className="input-small"
|
||||
placeholder="Filter by Name"
|
||||
icon="search"
|
||||
iconPosition="left"
|
||||
name="filter"
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
<InfoLine>
|
||||
<InfoLine.Point label={filtered.length} value=" requests" />
|
||||
<InfoLine.Point
|
||||
label={formatBytes(transferredSize)}
|
||||
value="transferred"
|
||||
display={transferredSize > 0}
|
||||
/>
|
||||
<InfoLine.Point
|
||||
label={formatBytes(resourcesSize)}
|
||||
value="resources"
|
||||
display={resourcesSize > 0}
|
||||
/>
|
||||
<InfoLine.Point
|
||||
label={formatMs(domBuildingTime)}
|
||||
value="DOM Building Time"
|
||||
display={domBuildingTime != null}
|
||||
/>
|
||||
<InfoLine.Point
|
||||
label={domContentLoadedTime && formatMs(domContentLoadedTime.value)}
|
||||
value="DOMContentLoaded"
|
||||
display={domContentLoadedTime != null}
|
||||
dotColor={DOM_LOADED_TIME_COLOR}
|
||||
/>
|
||||
<InfoLine.Point
|
||||
label={loadTime && formatMs(loadTime.value)}
|
||||
value="Load"
|
||||
display={loadTime != null}
|
||||
dotColor={LOAD_TIME_COLOR}
|
||||
/>
|
||||
</InfoLine>
|
||||
<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={filtered.length === 0}
|
||||
>
|
||||
<TimeTable
|
||||
rows={filtered}
|
||||
referenceLines={referenceLines}
|
||||
renderPopup
|
||||
// navigation
|
||||
onRowClick={onRowClick}
|
||||
additionalHeight={additionalHeight}
|
||||
activeIndex={lastIndex}
|
||||
>
|
||||
{[
|
||||
{
|
||||
label: 'Start',
|
||||
width: 90,
|
||||
render: renderStart,
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
dataKey: 'status',
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
dataKey: 'type',
|
||||
width: 90,
|
||||
render: renderType,
|
||||
},
|
||||
{
|
||||
label: 'Name',
|
||||
width: 240,
|
||||
render: renderName,
|
||||
},
|
||||
{
|
||||
label: 'Size',
|
||||
width: 60,
|
||||
render: renderSize,
|
||||
},
|
||||
{
|
||||
label: 'Time',
|
||||
width: 80,
|
||||
render: renderDuration,
|
||||
},
|
||||
]}
|
||||
</TimeTable>
|
||||
</NoContent>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,27 +11,29 @@ interface Props {
|
|||
active: boolean;
|
||||
}
|
||||
|
||||
export default function Marker({ target, active }: Props) {
|
||||
export default function Marker({ target, active }: Props) {
|
||||
const style = {
|
||||
top: `${ target.boundingRect.top }px`,
|
||||
left: `${ target.boundingRect.left }px`,
|
||||
width: `${ target.boundingRect.width }px`,
|
||||
height: `${ target.boundingRect.height }px`,
|
||||
}
|
||||
top: `${target.boundingRect.top}px`,
|
||||
left: `${target.boundingRect.left}px`,
|
||||
width: `${target.boundingRect.width}px`,
|
||||
height: `${target.boundingRect.height}px`,
|
||||
}
|
||||
return (
|
||||
<div className={ cn(stl.marker, { [stl.active] : active }) } style={ style } onClick={() => activeTarget(target.index)}>
|
||||
<div className={stl.index}>{target.index + 1}</div>
|
||||
<Tooltip
|
||||
open={active}
|
||||
arrow
|
||||
sticky
|
||||
distance={15}
|
||||
html={(
|
||||
<div>{target.count} Clicks</div>
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-0"></div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
<div className={cn(stl.marker, { [stl.active]: active })} style={style} onClick={() => activeTarget(target.index)}>
|
||||
<div className={stl.index}>{target.index + 1}</div>
|
||||
{/* @ts-expect-error Tooltip doesn't have children property */}
|
||||
<Tooltip
|
||||
open={active}
|
||||
arrow
|
||||
sticky
|
||||
distance={15}
|
||||
html={(
|
||||
<div>{target.count} Clicks</div>
|
||||
)}
|
||||
trigger="mouseenter"
|
||||
>
|
||||
<div className="absolute inset-0"></div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,85 +1,176 @@
|
|||
import { error as errorRoute } from 'App/routes';
|
||||
import JsonViewer from 'Components/Session_/StackEvents/UserEvent/JsonViewer';
|
||||
import Sentry from 'Components/Session_/StackEvents/UserEvent/Sentry';
|
||||
import { hideHint } from 'Duck/components/player';
|
||||
import withEnumToggle from 'HOCs/withEnumToggle';
|
||||
import { connectPlayer, jump } from 'Player';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { connectPlayer, jump } from 'Player';
|
||||
import { NoContent, Tabs } from 'UI';
|
||||
import withEnumToggle from 'HOCs/withEnumToggle';
|
||||
import { hideHint } from 'Duck/components/player';
|
||||
import { typeList } from 'Types/session/stackEvent';
|
||||
import UserEvent from './UserEvent';
|
||||
import { DATADOG, SENTRY, STACKDRIVER, typeList } from 'Types/session/stackEvent';
|
||||
import { NoContent, SlideModal, Tabs, Link } from 'UI';
|
||||
import Autoscroll from '../Autoscroll';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import UserEvent from './UserEvent';
|
||||
|
||||
const ALL = 'ALL';
|
||||
|
||||
const TABS = [ ALL, ...typeList ].map(tab =>({ text: tab, key: tab }));
|
||||
const TABS = [ALL, ...typeList].map((tab) => ({ text: tab, key: tab }));
|
||||
|
||||
@withEnumToggle('activeTab', 'setActiveTab', ALL)
|
||||
@connectPlayer(state => ({
|
||||
@connectPlayer((state) => ({
|
||||
stackEvents: state.stackList,
|
||||
stackEventsNow: state.stackListNow,
|
||||
}))
|
||||
@connect(state => ({
|
||||
hintIsHidden: state.getIn(['components', 'player', 'hiddenHints', 'stack']) ||
|
||||
!state.getIn([ 'site', 'list' ]).some(s => s.stackIntegrations),
|
||||
}), {
|
||||
hideHint
|
||||
})
|
||||
@connect(
|
||||
(state) => ({
|
||||
hintIsHidden:
|
||||
state.getIn(['components', 'player', 'hiddenHints', 'stack']) ||
|
||||
!state.getIn(['site', 'list']).some((s) => s.stackIntegrations),
|
||||
}),
|
||||
{
|
||||
hideHint,
|
||||
}
|
||||
)
|
||||
export default class StackEvents extends React.PureComponent {
|
||||
// onFilterChange = (e, { value }) => this.setState({ filter: value })
|
||||
// onFilterChange = (e, { value }) => this.setState({ filter: value })
|
||||
|
||||
state = {
|
||||
currentEvent: null,
|
||||
};
|
||||
|
||||
onDetailsClick(userEvent) {
|
||||
this.setState({ currentEvent: userEvent });
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.setState({ currentEvent: undefined });
|
||||
}
|
||||
|
||||
renderPopupContent(userEvent) {
|
||||
console.log('event', userEvent);
|
||||
const { source, payload, name } = userEvent;
|
||||
switch (source) {
|
||||
case SENTRY:
|
||||
return <Sentry event={payload} />;
|
||||
case DATADOG:
|
||||
return <JsonViewer title={name} data={payload} icon="integrations/datadog" />;
|
||||
case STACKDRIVER:
|
||||
return <JsonViewer title={name} data={payload} icon="integrations/stackdriver" />;
|
||||
default:
|
||||
return <JsonViewer title={name} data={payload} icon={`integrations/${source}`} />;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { stackEvents, activeTab, setActiveTab, hintIsHidden } = this.props;
|
||||
//const filterRE = new RegExp(filter, 'i');
|
||||
const { currentEvent } = this.state;
|
||||
|
||||
const tabs = TABS.filter(({ key }) => key === ALL || stackEvents.some(({ source }) => key === source));
|
||||
const tabs = TABS.filter(
|
||||
({ key }) => key === ALL || stackEvents.some(({ source }) => key === source)
|
||||
);
|
||||
|
||||
const filteredStackEvents = stackEvents
|
||||
// .filter(({ data }) => data.includes(filter))
|
||||
// .filter(({ data }) => data.includes(filter))
|
||||
.filter(({ source }) => activeTab === ALL || activeTab === source);
|
||||
|
||||
let lastIndex = -1;
|
||||
// TODO: Need to do filtering in store, or preferably in a selector
|
||||
filteredStackEvents.forEach((item, index) => {
|
||||
if (
|
||||
this.props.stackEventsNow.length > 0 &&
|
||||
item.time <= this.props.stackEventsNow[this.props.stackEventsNow.length - 1].time
|
||||
) {
|
||||
lastIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">Events</span>
|
||||
<Tabs
|
||||
className="uppercase"
|
||||
tabs={ tabs }
|
||||
active={ activeTab }
|
||||
onClick={ setActiveTab }
|
||||
border={ false }
|
||||
/>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
<NoContent
|
||||
title="Nothing to display yet."
|
||||
subtext={ !hintIsHidden
|
||||
?
|
||||
<>
|
||||
<a className="underline color-teal" href="https://docs.openreplay.com/integrations" target="_blank">Integrations</a>
|
||||
{' and '}
|
||||
<a className="underline color-teal" href="https://docs.openreplay.com/api#event" target="_blank">Events</a>
|
||||
{ ' make debugging easier. Sync your backend logs and custom events with session replay.' }
|
||||
<br/><br/>
|
||||
<button className="color-teal" onClick={() => this.props.hideHint("stack")}>Got It!</button>
|
||||
</>
|
||||
: null
|
||||
}
|
||||
size="small"
|
||||
show={ filteredStackEvents.length === 0 }
|
||||
>
|
||||
<Autoscroll>
|
||||
{ filteredStackEvents.map(userEvent => (
|
||||
<UserEvent
|
||||
key={ userEvent.key }
|
||||
userEvent={ userEvent }
|
||||
onJump={ () => jump(userEvent.time) }
|
||||
/>
|
||||
))}
|
||||
</Autoscroll>
|
||||
</NoContent>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
<>
|
||||
<SlideModal
|
||||
title={
|
||||
currentEvent && (
|
||||
<div className="mb-4">
|
||||
<div className="text-xl mb-2">
|
||||
<Link to={errorRoute(currentEvent.errorId)}>
|
||||
<span className="font-bold">{currentEvent.name}</span>
|
||||
</Link>
|
||||
<span className="ml-2 text-sm color-gray-medium">{currentEvent.function}</span>
|
||||
</div>
|
||||
<div>{currentEvent.message}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
isDisplayed={currentEvent != null}
|
||||
content={
|
||||
currentEvent && <div className="px-4">{this.renderPopupContent(currentEvent)}</div>
|
||||
}
|
||||
onClose={this.closeModal.bind(this)}
|
||||
/>
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">Events</span>
|
||||
<Tabs
|
||||
className="uppercase"
|
||||
tabs={tabs}
|
||||
active={activeTab}
|
||||
onClick={setActiveTab}
|
||||
border={false}
|
||||
/>
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
<NoContent
|
||||
title="Nothing to display yet."
|
||||
subtext={
|
||||
!hintIsHidden ? (
|
||||
<>
|
||||
<a
|
||||
className="underline color-teal"
|
||||
href="https://docs.openreplay.com/integrations"
|
||||
target="_blank"
|
||||
>
|
||||
Integrations
|
||||
</a>
|
||||
{' and '}
|
||||
<a
|
||||
className="underline color-teal"
|
||||
href="https://docs.openreplay.com/api#event"
|
||||
target="_blank"
|
||||
>
|
||||
Events
|
||||
</a>
|
||||
{
|
||||
' make debugging easier. Sync your backend logs and custom events with session replay.'
|
||||
}
|
||||
<br />
|
||||
<br />
|
||||
<button className="color-teal" onClick={() => this.props.hideHint('stack')}>
|
||||
Got It!
|
||||
</button>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
size="small"
|
||||
show={filteredStackEvents.length === 0}
|
||||
>
|
||||
<Autoscroll autoScrollTo={Math.max(lastIndex, 0)}>
|
||||
{filteredStackEvents.map((userEvent, index) => (
|
||||
<UserEvent
|
||||
key={userEvent.key}
|
||||
onDetailsClick={this.onDetailsClick.bind(this)}
|
||||
inactive={index > lastIndex}
|
||||
selected={lastIndex === index}
|
||||
userEvent={userEvent}
|
||||
onJump={() => jump(userEvent.time)}
|
||||
/>
|
||||
))}
|
||||
</Autoscroll>
|
||||
</NoContent>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,122 +1,68 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { OPENREPLAY, SENTRY, DATADOG, STACKDRIVER } from 'Types/session/stackEvent';
|
||||
import { Icon, SlideModal, IconButton } from 'UI';
|
||||
import { OPENREPLAY, SENTRY, DATADOG, STACKDRIVER } from 'Types/session/stackEvent';
|
||||
import { Icon, IconButton } from 'UI';
|
||||
import withToggle from 'HOCs/withToggle';
|
||||
import Sentry from './Sentry';
|
||||
import JsonViewer from './JsonViewer';
|
||||
import stl from './userEvent.module.css';
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
// const modalSources = [ SENTRY, DATADOG ];
|
||||
|
||||
@withToggle() //
|
||||
@withToggle() //
|
||||
export default class UserEvent extends React.PureComponent {
|
||||
getIconProps() {
|
||||
const { source } = this.props.userEvent;
|
||||
return {
|
||||
name: `integrations/${ source }`,
|
||||
size: 18,
|
||||
marginRight: source === OPENREPLAY ? 11 : 10
|
||||
}
|
||||
}
|
||||
getIconProps() {
|
||||
const { source } = this.props.userEvent;
|
||||
return {
|
||||
name: `integrations/${source}`,
|
||||
size: 18,
|
||||
marginRight: source === OPENREPLAY ? 11 : 10,
|
||||
};
|
||||
}
|
||||
|
||||
getLevelClassname() {
|
||||
const { userEvent } = this.props;
|
||||
if (userEvent.isRed()) return "error color-red";
|
||||
return '';
|
||||
}
|
||||
getLevelClassname() {
|
||||
const { userEvent } = this.props;
|
||||
if (userEvent.isRed()) return 'error color-red';
|
||||
return '';
|
||||
}
|
||||
|
||||
// getEventMessage() {
|
||||
// const { userEvent } = this.props;
|
||||
// switch(userEvent.source) {
|
||||
// case SENTRY:
|
||||
// case DATADOG:
|
||||
// return null;
|
||||
// default:
|
||||
// return JSON.stringify(userEvent.data);
|
||||
// }
|
||||
// }
|
||||
onClickDetails = (e) => {
|
||||
e.stopPropagation();
|
||||
this.props.onDetailsClick(this.props.userEvent);
|
||||
};
|
||||
|
||||
renderPopupContent() {
|
||||
const { userEvent: { source, payload, name} } = this.props;
|
||||
switch(source) {
|
||||
case SENTRY:
|
||||
return <Sentry event={ payload } />;
|
||||
case DATADOG:
|
||||
return <JsonViewer title={ name } data={ payload } icon="integrations/datadog" />;
|
||||
case STACKDRIVER:
|
||||
return <JsonViewer title={ name } data={ payload } icon="integrations/stackdriver" />;
|
||||
default:
|
||||
return <JsonViewer title={ name } data={ payload } icon={ `integrations/${ source }` } />;
|
||||
}
|
||||
}
|
||||
|
||||
ifNeedModal() {
|
||||
return !!this.props.userEvent.payload;
|
||||
}
|
||||
|
||||
onClickDetails = (e) => {
|
||||
e.stopPropagation();
|
||||
this.props.switchOpen();
|
||||
}
|
||||
|
||||
renderContent(modalTrigger) {
|
||||
const { userEvent } = this.props;
|
||||
//const message = this.getEventMessage();
|
||||
return (
|
||||
<div
|
||||
data-scroll-item={ userEvent.isRed() }
|
||||
// onClick={ this.props.switchOpen } //
|
||||
onClick={ this.props.onJump } //
|
||||
className={
|
||||
cn(
|
||||
"group",
|
||||
stl.userEvent,
|
||||
this.getLevelClassname(),
|
||||
{ [ stl.modalTrigger ]: modalTrigger }
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className={ stl.infoWrapper }>
|
||||
<div className={ stl.title } >
|
||||
<Icon { ...this.getIconProps() } />
|
||||
{ userEvent.name }
|
||||
</div>
|
||||
{ /* message &&
|
||||
<div className={ stl.message }>
|
||||
{ message }
|
||||
</div> */
|
||||
}
|
||||
<div className="invisible self-end ml-auto group-hover:visible">
|
||||
<IconButton size="small" plain onClick={this.onClickDetails} label="DETAILS" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { userEvent } = this.props;
|
||||
if (this.ifNeedModal()) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<SlideModal
|
||||
//title="Add Custom Field"
|
||||
size="middle"
|
||||
isDisplayed={ this.props.open }
|
||||
content={ this.props.open && this.renderPopupContent() }
|
||||
onClose={ this.props.switchOpen }
|
||||
/>
|
||||
{ this.renderContent(true) }
|
||||
</React.Fragment>
|
||||
//<Modal
|
||||
// trigger={ this.renderContent(true) }
|
||||
// content={ this.renderPopupContent() }
|
||||
// centered={ false }
|
||||
// size="small"
|
||||
// />
|
||||
);
|
||||
}
|
||||
return this.renderContent();
|
||||
}
|
||||
render() {
|
||||
const { userEvent, inactive, selected } = this.props;
|
||||
//const message = this.getEventMessage();
|
||||
return (
|
||||
<div
|
||||
data-scroll-item={userEvent.isRed()}
|
||||
// onClick={ this.props.switchOpen } //
|
||||
onClick={this.props.onJump} //
|
||||
className={cn('group flex py-2 px-4 ', stl.userEvent, this.getLevelClassname(), {
|
||||
[stl.inactive]: inactive,
|
||||
[stl.selected]: selected,
|
||||
})}
|
||||
>
|
||||
<div className={'self-start pr-4'}>
|
||||
{Duration.fromMillis(userEvent.time).toFormat('mm:ss.SSS')}
|
||||
</div>
|
||||
<div className={cn('mr-auto', stl.infoWrapper)}>
|
||||
<div className={stl.title}>
|
||||
<Icon {...this.getIconProps()} />
|
||||
{userEvent.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-center">
|
||||
<IconButton
|
||||
outline={!userEvent.isRed()}
|
||||
red={userEvent.isRed()}
|
||||
onClick={this.onClickDetails}
|
||||
label="DETAILS"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
.userEvent {
|
||||
border-radius: 3px;
|
||||
background-color: rgba(0, 118, 255, 0.05);
|
||||
font-family: 'Menlo', 'monaco', 'consolas', monospace;
|
||||
padding: 8px 10px;
|
||||
margin: 3px 0;
|
||||
|
||||
&.modalTrigger {
|
||||
cursor: pointer;
|
||||
|
|
@ -15,6 +12,7 @@
|
|||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-family: 'Menlo', 'monaco', 'consolas', monospace;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
|
@ -35,4 +33,12 @@
|
|||
&::-webkit-scrollbar {
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.inactive {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: $teal-light;
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Popup } from 'UI';
|
||||
import { percentOf } from 'App/utils';
|
||||
import styles from './barRow.module.css'
|
||||
import tableStyles from './timeTable.module.css';
|
||||
|
||||
const formatTime = time => time < 1000 ? `${ time.toFixed(2) }ms` : `${ time / 1000 }s`;
|
||||
|
||||
const BarRow = ({ resource: { time, ttfb = 0, duration, key }, popup=false, timestart = 0, timewidth }) => {
|
||||
const timeOffset = time - timestart;
|
||||
ttfb = ttfb || 0;
|
||||
const trigger = (
|
||||
<div
|
||||
className={ styles.barWrapper }
|
||||
style={ {
|
||||
left: `${ percentOf(timeOffset, timewidth) }%`,
|
||||
right: `${ 100 - percentOf(timeOffset + duration, timewidth) }%`,
|
||||
minWidth: '5px'
|
||||
} }
|
||||
>
|
||||
<div
|
||||
className={ styles.ttfbBar }
|
||||
style={ {
|
||||
width: `${ percentOf(ttfb, duration) }%`,
|
||||
} }
|
||||
/>
|
||||
<div
|
||||
className={ styles.downloadBar }
|
||||
style={ {
|
||||
width: `${ percentOf(duration - ttfb, duration) }%`,
|
||||
minWidth: '5px'
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
if (!popup) return <div key={ key } className={ tableStyles.row } > { trigger } </div>;
|
||||
|
||||
return (
|
||||
<div key={ key } className={ tableStyles.row } >
|
||||
<Popup
|
||||
basic
|
||||
style={{ width: '100%' }}
|
||||
unmountHTMLWhenHide
|
||||
content={
|
||||
<React.Fragment>
|
||||
{ ttfb != null &&
|
||||
<div className={ styles.popupRow }>
|
||||
<div className={ styles.title }>{ 'Waiting (TTFB)' }</div>
|
||||
<div className={ styles.popupBarWrapper} >
|
||||
<div
|
||||
className={ styles.ttfbBar }
|
||||
style={{
|
||||
left: 0,
|
||||
width: `${ percentOf(ttfb, duration) }%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={ styles.time } >{ formatTime(ttfb) }</div>
|
||||
</div>
|
||||
}
|
||||
<div className={ styles.popupRow }>
|
||||
<div className={ styles.title } >{ 'Content Download' }</div>
|
||||
<div className= { styles.popupBarWrapper }>
|
||||
<div
|
||||
className={ styles.downloadBar }
|
||||
style={{
|
||||
left: `${ percentOf(ttfb, duration) }%`,
|
||||
width: `${ percentOf(duration - ttfb, duration) }%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={ styles.time }>{ formatTime(duration - ttfb) }</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
{trigger}
|
||||
</Popup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BarRow.displayName = "BarRow";
|
||||
|
||||
export default BarRow;
|
||||
96
frontend/app/components/Session_/TimeTable/BarRow.tsx
Normal file
96
frontend/app/components/Session_/TimeTable/BarRow.tsx
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { Popup } from 'UI';
|
||||
import { percentOf } from 'App/utils';
|
||||
import styles from './barRow.module.css'
|
||||
import tableStyles from './timeTable.module.css';
|
||||
import React from 'react';
|
||||
|
||||
const formatTime = time => time < 1000 ? `${time.toFixed(2)}ms` : `${time / 1000}s`;
|
||||
|
||||
interface Props {
|
||||
resource: {
|
||||
time: number
|
||||
ttfb?: number
|
||||
duration?: number
|
||||
key: string
|
||||
}
|
||||
popup?: boolean
|
||||
timestart: number
|
||||
timewidth: number
|
||||
}
|
||||
|
||||
// TODO: If request has no duration, set duration to 0.2s. Enforce existence of duration in the future.
|
||||
const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = false, timestart = 0, timewidth }: Props) => {
|
||||
const timeOffset = time - timestart;
|
||||
ttfb = ttfb || 0;
|
||||
const trigger = (
|
||||
<div
|
||||
className={styles.barWrapper}
|
||||
style={{
|
||||
left: `${percentOf(timeOffset, timewidth)}%`,
|
||||
right: `${100 - percentOf(timeOffset + duration, timewidth)}%`,
|
||||
minWidth: '5px'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={styles.ttfbBar}
|
||||
style={{
|
||||
width: `${percentOf(ttfb, duration)}%`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={styles.downloadBar}
|
||||
style={{
|
||||
width: `${percentOf(duration - ttfb, duration)}%`,
|
||||
minWidth: '5px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
if (!popup) return <div key={key} className={tableStyles.row} > {trigger} </div>;
|
||||
|
||||
return (
|
||||
<div key={key} className={tableStyles.row} >
|
||||
<Popup
|
||||
basic
|
||||
content={
|
||||
<React.Fragment>
|
||||
{ttfb != null &&
|
||||
<div className={styles.popupRow}>
|
||||
<div className={styles.title}>{'Waiting (TTFB)'}</div>
|
||||
<div className={styles.popupBarWrapper} >
|
||||
<div
|
||||
className={styles.ttfbBar}
|
||||
style={{
|
||||
left: 0,
|
||||
width: `${percentOf(ttfb, duration)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.time} >{formatTime(ttfb)}</div>
|
||||
</div>
|
||||
}
|
||||
<div className={styles.popupRow}>
|
||||
<div className={styles.title} >{'Content Download'}</div>
|
||||
<div className={styles.popupBarWrapper}>
|
||||
<div
|
||||
className={styles.downloadBar}
|
||||
style={{
|
||||
left: `${percentOf(ttfb, duration)}%`,
|
||||
width: `${percentOf(duration - ttfb, duration)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.time}>{formatTime(duration - ttfb)}</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
size="mini"
|
||||
position="top center"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BarRow.displayName = "BarRow";
|
||||
|
||||
export default BarRow;
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import { List, AutoSizer } from 'react-virtualized';
|
||||
import cn from 'classnames';
|
||||
import { Duration } from "luxon";
|
||||
import { NoContent, IconButton, Button } from 'UI';
|
||||
import { percentOf } from 'App/utils';
|
||||
import { formatMs } from 'App/date';
|
||||
|
||||
import BarRow from './BarRow';
|
||||
import stl from './timeTable.module.css';
|
||||
|
|
@ -11,31 +11,35 @@ import stl from './timeTable.module.css';
|
|||
import autoscrollStl from '../autoscroll.module.css'; //aaa
|
||||
|
||||
type Timed = {
|
||||
time: number;
|
||||
time: number;
|
||||
};
|
||||
|
||||
type Durationed = {
|
||||
duration: number;
|
||||
duration: number;
|
||||
};
|
||||
|
||||
type CanBeRed = {
|
||||
//+isRed: boolean,
|
||||
isRed: () => boolean;
|
||||
//+isRed: boolean,
|
||||
isRed: () => boolean;
|
||||
};
|
||||
|
||||
type Row = Timed & Durationed & CanBeRed;
|
||||
interface Row extends Timed, Durationed, CanBeRed {
|
||||
[key: string]: any, key: string
|
||||
}
|
||||
|
||||
type Line = {
|
||||
color: string; // Maybe use typescript?
|
||||
hint?: string;
|
||||
onClick?: any;
|
||||
color: string; // Maybe use typescript?
|
||||
hint?: string;
|
||||
onClick?: any;
|
||||
} & Timed;
|
||||
|
||||
type Column = {
|
||||
label: string;
|
||||
width: number;
|
||||
referenceLines?: Array<Line>;
|
||||
style?: Object;
|
||||
label: string;
|
||||
width: number;
|
||||
dataKey?: string;
|
||||
render?: (row: any) => void
|
||||
referenceLines?: Array<Line>;
|
||||
style?: React.CSSProperties;
|
||||
} & RenderOrKey;
|
||||
|
||||
// type RenderOrKey = { // Disjoint?
|
||||
|
|
@ -44,23 +48,31 @@ type Column = {
|
|||
// dataKey: string,
|
||||
// }
|
||||
type RenderOrKey =
|
||||
| {
|
||||
render?: (row: Row) => React.ReactNode;
|
||||
key?: string;
|
||||
}
|
||||
| {
|
||||
dataKey: string;
|
||||
};
|
||||
| {
|
||||
render?: (row: Row) => React.ReactNode;
|
||||
key?: string;
|
||||
}
|
||||
| {
|
||||
dataKey: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
rows: Array<Row>;
|
||||
children: Array<Column>;
|
||||
className?: string;
|
||||
rows: Array<Row>;
|
||||
children: Array<Column>;
|
||||
tableHeight?: number
|
||||
activeIndex?: number
|
||||
renderPopup?: boolean
|
||||
navigation?: boolean
|
||||
referenceLines?: any[]
|
||||
additionalHeight?: number
|
||||
hoverable?: boolean
|
||||
onRowClick?: (row: any, index: number) => void
|
||||
};
|
||||
|
||||
type TimeLineInfo = {
|
||||
timestart: number;
|
||||
timewidth: number;
|
||||
timestart: number;
|
||||
timewidth: number;
|
||||
};
|
||||
|
||||
type State = TimeLineInfo & typeof initialState;
|
||||
|
|
@ -72,247 +84,235 @@ const ROW_HEIGHT = 32;
|
|||
|
||||
const TIME_SECTIONS_COUNT = 8;
|
||||
const ZERO_TIMEWIDTH = 1000;
|
||||
function formatTime(ms) {
|
||||
if (ms < 0) return '';
|
||||
return formatMs(ms);
|
||||
function formatTime(ms: number) {
|
||||
if (ms < 0) return '';
|
||||
if (ms < 1000) return Duration.fromMillis(ms).toFormat('0.SSS')
|
||||
return Duration.fromMillis(ms).toFormat('mm:ss');
|
||||
}
|
||||
|
||||
function computeTimeLine(rows: Array<Row>, firstVisibleRowIndex: number, visibleCount): TimeLineInfo {
|
||||
const visibleRows = rows.slice(firstVisibleRowIndex, firstVisibleRowIndex + visibleCount + _additionalHeight);
|
||||
let timestart = visibleRows.length > 0 ? Math.min(...visibleRows.map((r) => r.time)) : 0;
|
||||
const timeend = visibleRows.length > 0 ? Math.max(...visibleRows.map((r) => r.time + r.duration)) : 0;
|
||||
let timewidth = timeend - timestart;
|
||||
const offset = timewidth / 70;
|
||||
if (timestart >= offset) {
|
||||
timestart -= offset;
|
||||
}
|
||||
timewidth *= 1.5; // += offset;
|
||||
if (timewidth === 0) {
|
||||
timewidth = ZERO_TIMEWIDTH;
|
||||
}
|
||||
return {
|
||||
timestart,
|
||||
timewidth,
|
||||
};
|
||||
function computeTimeLine(rows: Array<Row>, firstVisibleRowIndex: number, visibleCount: number): TimeLineInfo {
|
||||
const visibleRows = rows.slice(firstVisibleRowIndex, firstVisibleRowIndex + visibleCount + _additionalHeight);
|
||||
let timestart = visibleRows.length > 0 ? Math.min(...visibleRows.map((r) => r.time)) : 0;
|
||||
// TODO: GraphQL requests do not have a duration, so their timeline is borked. Assume a duration of 0.2s for every GraphQL request
|
||||
const timeend = visibleRows.length > 0 ? Math.max(...visibleRows.map((r) => r.time + (r.duration ?? 200))) : 0;
|
||||
let timewidth = timeend - timestart;
|
||||
const offset = timewidth / 70;
|
||||
if (timestart >= offset) {
|
||||
timestart -= offset;
|
||||
}
|
||||
timewidth *= 1.5; // += offset;
|
||||
if (timewidth === 0) {
|
||||
timewidth = ZERO_TIMEWIDTH;
|
||||
}
|
||||
return {
|
||||
timestart,
|
||||
timewidth,
|
||||
};
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
firstVisibleRowIndex: 0,
|
||||
firstVisibleRowIndex: 0,
|
||||
};
|
||||
|
||||
export default class TimeTable extends React.PureComponent<Props, State> {
|
||||
state = {
|
||||
...computeTimeLine(this.props.rows, initialState.firstVisibleRowIndex, this.visibleCount),
|
||||
...initialState,
|
||||
};
|
||||
state = {
|
||||
...computeTimeLine(this.props.rows, initialState.firstVisibleRowIndex, this.visibleCount),
|
||||
...initialState,
|
||||
};
|
||||
|
||||
get tableHeight() {
|
||||
return this.props.tableHeight || 195;
|
||||
get tableHeight() {
|
||||
return this.props.tableHeight || 195;
|
||||
}
|
||||
|
||||
get visibleCount() {
|
||||
return Math.ceil(this.tableHeight / ROW_HEIGHT);
|
||||
}
|
||||
|
||||
scroller = React.createRef<List>();
|
||||
autoScroll = true;
|
||||
|
||||
componentDidMount() {
|
||||
if (this.scroller.current) {
|
||||
this.scroller.current.scrollToRow(this.props.activeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: any, prevState: any) {
|
||||
if (
|
||||
prevState.firstVisibleRowIndex !== this.state.firstVisibleRowIndex ||
|
||||
(this.props.rows.length <= this.visibleCount + _additionalHeight && prevProps.rows.length !== this.props.rows.length)
|
||||
) {
|
||||
this.setState({
|
||||
...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);
|
||||
}
|
||||
}
|
||||
|
||||
onScroll = ({ scrollTop, scrollHeight, clientHeight }: { scrollTop: number; scrollHeight: number; clientHeight: number }): void => {
|
||||
const firstVisibleRowIndex = Math.floor(scrollTop / ROW_HEIGHT + 0.33);
|
||||
|
||||
if (this.state.firstVisibleRowIndex !== firstVisibleRowIndex) {
|
||||
this.autoScroll = scrollHeight - clientHeight - scrollTop < ROW_HEIGHT / 2;
|
||||
this.setState({ firstVisibleRowIndex });
|
||||
}
|
||||
};
|
||||
|
||||
renderRow = ({ index, key, style: rowStyle }: any) => {
|
||||
const { activeIndex } = this.props;
|
||||
const { children: columns, rows, renderPopup, hoverable, onRowClick } = this.props;
|
||||
const { timestart, timewidth } = this.state;
|
||||
const row = rows[index];
|
||||
return (
|
||||
<div
|
||||
style={rowStyle}
|
||||
key={key}
|
||||
className={cn('border-b border-color-gray-light-shade', 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.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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
onPrevClick = () => {
|
||||
let prevRedIndex = -1;
|
||||
for (let i = this.state.firstVisibleRowIndex - 1; i >= 0; i--) {
|
||||
if (this.props.rows[i].isRed()) {
|
||||
prevRedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this.scroller.current != null) {
|
||||
this.scroller.current.scrollToRow(prevRedIndex);
|
||||
}
|
||||
};
|
||||
|
||||
onNextClick = () => {
|
||||
let prevRedIndex = -1;
|
||||
for (let i = this.state.firstVisibleRowIndex + 1; i < this.props.rows.length; i++) {
|
||||
if (this.props.rows[i].isRed()) {
|
||||
prevRedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this.scroller.current != null) {
|
||||
this.scroller.current.scrollToRow(prevRedIndex);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, rows, children: columns, navigation = false, referenceLines = [], additionalHeight = 0, activeIndex } = this.props;
|
||||
const { timewidth, timestart } = this.state;
|
||||
|
||||
_additionalHeight = additionalHeight;
|
||||
|
||||
const sectionDuration = Math.round(timewidth / TIME_SECTIONS_COUNT);
|
||||
const timeColumns: number[] = [];
|
||||
if (timewidth > 0) {
|
||||
for (let i = 0; i < TIME_SECTIONS_COUNT; i++) {
|
||||
timeColumns.push(timestart + i * sectionDuration);
|
||||
}
|
||||
}
|
||||
|
||||
get visibleCount() {
|
||||
return Math.ceil(this.tableHeight / ROW_HEIGHT);
|
||||
}
|
||||
const visibleRefLines = referenceLines.filter(({ time }) => time > timestart && time < timestart + timewidth);
|
||||
|
||||
scroller = React.createRef();
|
||||
autoScroll = true;
|
||||
const columnsSumWidth = columns.reduce((sum, { width }) => sum + width, 0);
|
||||
|
||||
componentDidMount() {
|
||||
if (this.scroller.current) {
|
||||
this.scroller.current.scrollToRow(this.props.activeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: any, prevState: any) {
|
||||
// if (prevProps.rows.length !== this.props.rows.length &&
|
||||
// this.autoScroll &&
|
||||
// this.scroller.current != null) {
|
||||
// this.scroller.current.scrollToRow(this.props.rows.length);
|
||||
// }
|
||||
if (
|
||||
prevState.firstVisibleRowIndex !== this.state.firstVisibleRowIndex ||
|
||||
(this.props.rows.length <= this.visibleCount + _additionalHeight && prevProps.rows.length !== this.props.rows.length)
|
||||
) {
|
||||
this.setState({
|
||||
...computeTimeLine(this.props.rows, this.state.firstVisibleRowIndex, this.visibleCount),
|
||||
});
|
||||
}
|
||||
if (this.props.activeIndex >= 0 && prevProps.activeIndex !== this.props.activeIndex && this.scroller.current) {
|
||||
this.scroller.current.scrollToRow(this.props.activeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
onScroll = ({ scrollTop, scrollHeight, clientHeight }: { scrollTop: number; scrollHeight: number; clientHeight: number }): void => {
|
||||
const firstVisibleRowIndex = Math.floor(scrollTop / ROW_HEIGHT + 0.33);
|
||||
|
||||
if (this.state.firstVisibleRowIndex !== firstVisibleRowIndex) {
|
||||
this.autoScroll = scrollHeight - clientHeight - scrollTop < ROW_HEIGHT / 2;
|
||||
this.setState({ firstVisibleRowIndex });
|
||||
}
|
||||
};
|
||||
|
||||
renderRow = ({ index, key, style: rowStyle }: any) => {
|
||||
const { activeIndex } = this.props;
|
||||
const { children: columns, rows, renderPopup, hoverable, onRowClick } = this.props;
|
||||
const { timestart, timewidth } = this.state;
|
||||
const row = rows[index];
|
||||
return (
|
||||
<div
|
||||
style={rowStyle}
|
||||
key={key}
|
||||
className={cn('border-b border-color-gray-light-shade', stl.row, {
|
||||
[stl.hoverable]: hoverable,
|
||||
'error color-red': !!row.isRed && row.isRed(),
|
||||
'cursor-pointer': typeof onRowClick === 'function',
|
||||
[stl.activeRow]: activeIndex === index,
|
||||
})}
|
||||
onClick={typeof onRowClick === 'function' ? () => onRowClick(row, index) : null}
|
||||
id="table-row"
|
||||
>
|
||||
{columns.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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
onPrevClick = () => {
|
||||
let prevRedIndex = -1;
|
||||
for (let i = this.state.firstVisibleRowIndex - 1; i >= 0; i--) {
|
||||
if (this.props.rows[i].isRed()) {
|
||||
prevRedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this.scroller.current != null) {
|
||||
this.scroller.current.scrollToRow(prevRedIndex);
|
||||
}
|
||||
};
|
||||
|
||||
onNextClick = () => {
|
||||
let prevRedIndex = -1;
|
||||
for (let i = this.state.firstVisibleRowIndex + 1; i < this.props.rows.length; i++) {
|
||||
if (this.props.rows[i].isRed()) {
|
||||
prevRedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this.scroller.current != null) {
|
||||
this.scroller.current.scrollToRow(prevRedIndex);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, rows, children: columns, navigation = false, referenceLines = [], additionalHeight = 0, activeIndex } = this.props;
|
||||
const { timewidth, timestart } = this.state;
|
||||
|
||||
_additionalHeight = additionalHeight;
|
||||
|
||||
const sectionDuration = Math.round(timewidth / TIME_SECTIONS_COUNT);
|
||||
const timeColumns = [];
|
||||
if (timewidth > 0) {
|
||||
for (let i = 0; i < TIME_SECTIONS_COUNT; i++) {
|
||||
timeColumns.push(timestart + i * sectionDuration);
|
||||
}
|
||||
}
|
||||
|
||||
const visibleRefLines = referenceLines.filter(({ time }) => time > timestart && time < timestart + timewidth);
|
||||
|
||||
const columnsSumWidth = columns.reduce((sum, { width }) => sum + width, 0);
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'relative')}>
|
||||
{navigation && (
|
||||
<div className={cn(autoscrollStl.navButtons, 'flex items-center')}>
|
||||
<Button
|
||||
variant="text-primary"
|
||||
icon="chevron-up"
|
||||
tooltip={{
|
||||
title: 'Previous Error',
|
||||
delay: 0,
|
||||
}}
|
||||
onClick={this.onPrevClick}
|
||||
/>
|
||||
<Button
|
||||
variant="text-primary"
|
||||
icon="chevron-down"
|
||||
tooltip={{
|
||||
title: 'Next Error',
|
||||
delay: 0,
|
||||
}}
|
||||
onClick={this.onNextClick}
|
||||
/>
|
||||
{/* <IconButton
|
||||
size="small"
|
||||
icon="chevron-up"
|
||||
return (
|
||||
<div className={cn(className, 'relative')}>
|
||||
{navigation && (
|
||||
<div className={cn(autoscrollStl.navButtons, 'flex items-center')}>
|
||||
<Button
|
||||
variant="text-primary"
|
||||
icon="chevron-up"
|
||||
tooltip={{
|
||||
title: 'Previous Error',
|
||||
delay: 0,
|
||||
}}
|
||||
onClick={this.onPrevClick}
|
||||
/> */}
|
||||
{/* <IconButton
|
||||
size="small"
|
||||
/>
|
||||
<Button
|
||||
variant="text-primary"
|
||||
icon="chevron-down"
|
||||
tooltip={{
|
||||
title: 'Next Error',
|
||||
delay: 0,
|
||||
}}
|
||||
onClick={this.onNextClick}
|
||||
/> */}
|
||||
</div>
|
||||
)}
|
||||
<div className={stl.headers}>
|
||||
<div className={stl.infoHeaders}>
|
||||
{columns.map(({ label, width }) => (
|
||||
<div className={stl.headerCell} style={{ width: `${width}px` }}>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={stl.waterfallHeaders}>
|
||||
{timeColumns.map((time, i) => (
|
||||
<div className={stl.timeCell} key={`tc-${i}`}>
|
||||
{formatTime(time)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={stl.headers}>
|
||||
<div className={stl.infoHeaders}>
|
||||
{columns.map(({ label, width }) => (
|
||||
<div className={stl.headerCell} style={{ width: `${width}px` }}>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={stl.waterfallHeaders}>
|
||||
{timeColumns.map((time, i) => (
|
||||
<div className={stl.timeCell} key={`tc-${i}`}>
|
||||
{formatTime(time)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NoContent size="small" show={rows.length === 0} title="No recordings found">
|
||||
<div className="relative">
|
||||
<div className={stl.timePart} style={{ left: `${columnsSumWidth}px` }}>
|
||||
{timeColumns.map((_, index) => (
|
||||
<div key={`tc-${index}`} className={stl.timeCell} />
|
||||
))}
|
||||
{visibleRefLines.map(({ time, color, onClick }) => (
|
||||
<div
|
||||
className={cn(stl.refLine, `bg-${color}`)}
|
||||
style={{
|
||||
left: `${percentOf(time - timestart, timewidth)}%`,
|
||||
cursor: typeof onClick === 'function' ? 'click' : 'auto',
|
||||
}}
|
||||
onClick={onClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
<List
|
||||
ref={this.scroller}
|
||||
className={stl.list}
|
||||
height={this.tableHeight + additionalHeight}
|
||||
width={width}
|
||||
overscanRowCount={20}
|
||||
rowCount={rows.length}
|
||||
rowHeight={ROW_HEIGHT}
|
||||
rowRenderer={this.renderRow}
|
||||
onScroll={this.onScroll}
|
||||
scrollToAlignment="start"
|
||||
forceUpdateProp={timestart | timewidth | activeIndex}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</NoContent>
|
||||
<NoContent size="small" show={rows.length === 0}>
|
||||
<div className="relative">
|
||||
<div className={stl.timePart} style={{ left: `${columnsSumWidth}px` }}>
|
||||
{timeColumns.map((_, index) => (
|
||||
<div key={`tc-${index}`} className={stl.timeCell} />
|
||||
))}
|
||||
{visibleRefLines.map(({ time, color, onClick }) => (
|
||||
<div
|
||||
className={cn(stl.refLine, `bg-${color}`)}
|
||||
style={{
|
||||
left: `${percentOf(time - timestart, timewidth)}%`,
|
||||
cursor: typeof onClick === 'function' ? 'click' : 'auto',
|
||||
}}
|
||||
onClick={onClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }: { width: number }) => (
|
||||
<List
|
||||
ref={this.scroller}
|
||||
className={stl.list}
|
||||
height={this.tableHeight + additionalHeight}
|
||||
width={width}
|
||||
overscanRowCount={20}
|
||||
rowCount={rows.length}
|
||||
rowHeight={ROW_HEIGHT}
|
||||
rowRenderer={this.renderRow}
|
||||
onScroll={this.onScroll}
|
||||
scrollToAlignment="start"
|
||||
forceUpdateProp={timestart | timewidth | (activeIndex || 0)}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</NoContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -100,5 +100,9 @@ $offset: 10px;
|
|||
}
|
||||
|
||||
.activeRow {
|
||||
background-color: rgba(54, 108, 217, 0.1);
|
||||
background-color: $teal-light;
|
||||
}
|
||||
|
||||
.inactiveRow {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
|
@ -1,19 +1,12 @@
|
|||
.wrapper {
|
||||
& .navButtons {
|
||||
opacity: 0;
|
||||
transition: opacity .3s
|
||||
}
|
||||
&:hover {
|
||||
& .navButtons {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navButtons {
|
||||
position: absolute;
|
||||
right: 260px;
|
||||
top: -39px;
|
||||
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
padding: 4px;
|
||||
|
||||
right: 24px;
|
||||
top: 8px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,60 +1,55 @@
|
|||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import {
|
||||
removeQueryParams,
|
||||
addQueryParams,
|
||||
setQueryParams,
|
||||
parseQuery,
|
||||
} from 'App/routes';
|
||||
import { removeQueryParams, addQueryParams, setQueryParams, parseQuery } from 'App/routes';
|
||||
|
||||
/* eslint-disable react/sort-comp */
|
||||
|
||||
const withLocationHandlers = propNames => BaseComponent =>
|
||||
const withLocationHandlers = (propNames) => (BaseComponent) => {
|
||||
@withRouter
|
||||
class extends React.Component {
|
||||
getQuery = names => parseQuery(this.props.location, names)
|
||||
getParam = name => parseQuery(this.props.location)[ name ]
|
||||
class WrapperClass extends React.Component {
|
||||
getQuery = (names) => parseQuery(this.props.location, names);
|
||||
getParam = (name) => parseQuery(this.props.location)[name];
|
||||
addQuery = (params) => {
|
||||
const { location, history } = this.props;
|
||||
history.push(addQueryParams(location, params));
|
||||
}
|
||||
removeQuery = (names = [], replace=false) => {
|
||||
};
|
||||
removeQuery = (names = [], replace = false) => {
|
||||
const { location, history } = this.props;
|
||||
|
||||
const namesArray = Array.isArray(names) ? names : [ names ];
|
||||
const namesArray = Array.isArray(names) ? names : [names];
|
||||
/* to avoid update stack overflow */
|
||||
const actualNames = Object.keys(this.getQuery(namesArray));
|
||||
|
||||
if (actualNames.length > 0) {
|
||||
history[ replace ? 'replace' : 'push' ](removeQueryParams(location, actualNames));
|
||||
history[replace ? 'replace' : 'push'](removeQueryParams(location, actualNames));
|
||||
}
|
||||
}
|
||||
setQuery = (params, replace=false) => {
|
||||
};
|
||||
setQuery = (params, replace = false) => {
|
||||
const { location, history } = this.props;
|
||||
history[ replace ? 'replace' : 'push' ](setQueryParams(location, params));
|
||||
}
|
||||
history[replace ? 'replace' : 'push'](setQueryParams(location, params));
|
||||
};
|
||||
query = {
|
||||
all: this.getQuery,
|
||||
get: this.getParam,
|
||||
add: this.addQuery,
|
||||
remove: this.removeQuery,
|
||||
set: this.setQuery, // TODO: use namespaces
|
||||
}
|
||||
set: this.setQuery, // TODO: use namespaces
|
||||
};
|
||||
|
||||
getHash = () => this.props.location.hash.substring(1)
|
||||
getHash = () => this.props.location.hash.substring(1);
|
||||
setHash = (hash) => {
|
||||
const { location, history } = this.props;
|
||||
history.push({ ...location, hash: `#${ hash }` });
|
||||
}
|
||||
history.push({ ...location, hash: `#${hash}` });
|
||||
};
|
||||
removeHash = () => {
|
||||
const { location, history } = this.props;
|
||||
history.push({ ...location, hash: '' });
|
||||
}
|
||||
};
|
||||
hash = {
|
||||
get: this.getHash,
|
||||
set: this.setHash,
|
||||
remove: this.removeHash,
|
||||
}
|
||||
};
|
||||
|
||||
getQueryProps() {
|
||||
if (Array.isArray(propNames)) return this.getQuery(propNames);
|
||||
|
|
@ -62,7 +57,9 @@ const withLocationHandlers = propNames => BaseComponent =>
|
|||
const values = Object.values(propNames);
|
||||
const query = this.getQuery(values);
|
||||
const queryProps = {};
|
||||
Object.keys(propNames).map((key) => { queryProps[ key ] = query[ propNames[ key ] ]; });
|
||||
Object.keys(propNames).map((key) => {
|
||||
queryProps[key] = query[propNames[key]];
|
||||
});
|
||||
return queryProps;
|
||||
}
|
||||
return {};
|
||||
|
|
@ -70,15 +67,9 @@ const withLocationHandlers = propNames => BaseComponent =>
|
|||
|
||||
render() {
|
||||
const queryProps = this.getQueryProps();
|
||||
return (
|
||||
<BaseComponent
|
||||
query={ this.query }
|
||||
hash={ this.hash }
|
||||
{ ...queryProps }
|
||||
{ ...this.props }
|
||||
/>
|
||||
);
|
||||
return <BaseComponent query={this.query} hash={this.hash} {...queryProps} {...this.props} />;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
return WrapperClass;
|
||||
};
|
||||
export default withLocationHandlers;
|
||||
|
|
|
|||
|
|
@ -2,33 +2,32 @@ import React from "react";
|
|||
import { connect } from "react-redux";
|
||||
import { NoPermission, NoSessionPermission } from "UI";
|
||||
|
||||
export default (requiredPermissions, className, isReplay = false) =>
|
||||
(BaseComponent) =>
|
||||
(
|
||||
@connect((state, props) => ({
|
||||
permissions:
|
||||
state.getIn(["user", "account", "permissions"]) || [],
|
||||
isEnterprise:
|
||||
state.getIn(["user", "account", "edition"]) === "ee",
|
||||
}))
|
||||
class extends React.PureComponent {
|
||||
render() {
|
||||
const hasPermission = requiredPermissions.every(
|
||||
(permission) =>
|
||||
this.props.permissions.includes(permission)
|
||||
);
|
||||
export default (requiredPermissions, className, isReplay = false) => (BaseComponent) => {
|
||||
@connect((state, props) => ({
|
||||
permissions:
|
||||
state.getIn(["user", "account", "permissions"]) || [],
|
||||
isEnterprise:
|
||||
state.getIn(["user", "account", "edition"]) === "ee",
|
||||
}))
|
||||
class WrapperClass extends React.PureComponent {
|
||||
render() {
|
||||
const hasPermission = requiredPermissions.every(
|
||||
(permission) =>
|
||||
this.props.permissions.includes(permission)
|
||||
);
|
||||
|
||||
return !this.props.isEnterprise || hasPermission ? (
|
||||
<BaseComponent {...this.props} />
|
||||
) : (
|
||||
<div className={className}>
|
||||
{isReplay ? (
|
||||
<NoSessionPermission />
|
||||
) : (
|
||||
<NoPermission />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
return !this.props.isEnterprise || hasPermission ? (
|
||||
<BaseComponent {...this.props} />
|
||||
) : (
|
||||
<div className={className}>
|
||||
{isReplay ? (
|
||||
<NoSessionPermission />
|
||||
) : (
|
||||
<NoPermission />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return WrapperClass
|
||||
}
|
||||
|
|
@ -1,30 +1,32 @@
|
|||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { withSiteId } from 'App/routes';
|
||||
import { withSiteId } from 'App/routes';
|
||||
import { setSiteId } from 'Duck/site';
|
||||
|
||||
export default BaseComponent =>
|
||||
@withRouter
|
||||
@connect((state, props) => ({
|
||||
urlSiteId: props.match.params.siteId,
|
||||
siteId: state.getIn([ 'site', 'siteId' ]),
|
||||
}), {
|
||||
setSiteId,
|
||||
})
|
||||
class extends React.PureComponent {
|
||||
push = (location) => {
|
||||
const { history, siteId } = this.props;
|
||||
if (typeof location === 'string') {
|
||||
history.push(withSiteId(location, siteId));
|
||||
} else if (typeof location === 'object'){
|
||||
history.push({ ...location, pathname: withSiteId(location.pathname, siteId) });
|
||||
export default BaseComponent => {
|
||||
@withRouter
|
||||
@connect((state, props) => ({
|
||||
urlSiteId: props.match.params.siteId,
|
||||
siteId: state.getIn(['site', 'siteId']),
|
||||
}), {
|
||||
setSiteId,
|
||||
})
|
||||
class WrappedClass extends React.PureComponent {
|
||||
push = (location) => {
|
||||
const { history, siteId } = this.props;
|
||||
if (typeof location === 'string') {
|
||||
history.push(withSiteId(location, siteId));
|
||||
} else if (typeof location === 'object') {
|
||||
history.push({ ...location, pathname: withSiteId(location.pathname, siteId) });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { history, ...other } = this.props
|
||||
|
||||
return <BaseComponent {...other} history={{ ...history, push: this.push }} />
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { history, ...other } = this.props
|
||||
|
||||
return <BaseComponent { ...other } history={ { ...history, push: this.push } } />
|
||||
}
|
||||
}
|
||||
return WrappedClass
|
||||
}
|
||||
|
|
@ -2,36 +2,39 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { setSiteId } from 'Duck/site';
|
||||
|
||||
export default BaseComponent =>
|
||||
@connect((state, props) => ({
|
||||
urlSiteId: props.match.params.siteId,
|
||||
siteId: state.getIn([ 'site', 'siteId' ]),
|
||||
}), {
|
||||
setSiteId,
|
||||
})
|
||||
class extends React.PureComponent {
|
||||
state = { load: false }
|
||||
constructor(props) {
|
||||
super(props);
|
||||
if (props.urlSiteId && props.urlSiteId !== props.siteId) {
|
||||
props.setSiteId(props.urlSiteId);
|
||||
export default (BaseComponent) => {
|
||||
@connect((state, props) => ({
|
||||
urlSiteId: props.match.params.siteId,
|
||||
siteId: state.getIn(['site', 'siteId']),
|
||||
}), {
|
||||
setSiteId,
|
||||
})
|
||||
class WrapperClass extends React.PureComponent {
|
||||
state = { load: false }
|
||||
constructor(props) {
|
||||
super(props);
|
||||
if (props.urlSiteId && props.urlSiteId !== props.siteId) {
|
||||
props.setSiteId(props.urlSiteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
const { urlSiteId, siteId, location: { pathname }, history } = this.props;
|
||||
const shouldUrlUpdate = urlSiteId && urlSiteId !== siteId;
|
||||
if (shouldUrlUpdate) {
|
||||
const path = [ '', siteId ].concat(pathname.split('/').slice(2)).join('/');
|
||||
history.push(path);
|
||||
componentDidUpdate(prevProps) {
|
||||
const { urlSiteId, siteId, location: { pathname }, history } = this.props;
|
||||
const shouldUrlUpdate = urlSiteId && urlSiteId !== siteId;
|
||||
if (shouldUrlUpdate) {
|
||||
const path = ['', siteId].concat(pathname.split('/').slice(2)).join('/');
|
||||
history.push(path);
|
||||
}
|
||||
const shouldBaseComponentReload = shouldUrlUpdate || siteId !== prevProps.siteId;
|
||||
if (shouldBaseComponentReload) {
|
||||
this.setState({ load: true });
|
||||
setTimeout(() => this.setState({ load: false }), 0);
|
||||
}
|
||||
}
|
||||
const shouldBaseComponentReload = shouldUrlUpdate || siteId !== prevProps.siteId;
|
||||
if (shouldBaseComponentReload) {
|
||||
this.setState({ load: true });
|
||||
setTimeout(() => this.setState({ load: false }), 0);
|
||||
|
||||
render() {
|
||||
return this.state.load ? null : <BaseComponent {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.load ? null : <BaseComponent { ...this.props } />;
|
||||
}
|
||||
}
|
||||
return WrapperClass
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ function SeriesName(props: Props) {
|
|||
onFocus={() => setEditing(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-base h-8 flex items-center border-transparent">{name.trim() === '' ? 'Seriess ' + (seriesIndex + 1) : name }</div>
|
||||
<div className="text-base h-8 flex items-center border-transparent">{name && name.trim() === '' ? 'Series ' + (seriesIndex + 1) : name }</div>
|
||||
)}
|
||||
|
||||
<div className="ml-3 cursor-pointer" onClick={() => setEditing(true)}><Icon name="pencil" size="14" /></div>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,33 @@
|
|||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { IconButton } from 'UI'
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { IconButton } from 'UI';
|
||||
import stl from './errorItem.module.css';
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
function ErrorItem({ error = {}, onErrorClick, onJump }) {
|
||||
function ErrorItem({ error = {}, onErrorClick, onJump, inactive, selected }) {
|
||||
return (
|
||||
<div className={ cn(stl.wrapper, 'py-3 px-4 flex cursor-pointer') } onClick={onJump}>
|
||||
<div className="mr-auto">
|
||||
<div
|
||||
className={cn(stl.wrapper, 'py-2 px-4 flex cursor-pointer', {
|
||||
[stl.inactive]: inactive,
|
||||
[stl.selected]: selected,
|
||||
})}
|
||||
onClick={onJump}
|
||||
>
|
||||
<div className={'self-start pr-4 color-red'}>
|
||||
{Duration.fromMillis(error.time).toFormat('mm:ss.SSS')}
|
||||
</div>
|
||||
<div className="mr-auto overflow-hidden">
|
||||
<div className="color-red mb-1 cursor-pointer code-font">
|
||||
{error.name}
|
||||
<span className="color-gray-darkest ml-2">{ error.stack0InfoString }</span>
|
||||
<span className="color-gray-darkest ml-2">{error.stack0InfoString}</span>
|
||||
</div>
|
||||
<div className="text-sm color-gray-medium">{error.message}</div>
|
||||
</div>
|
||||
<div className="self-end">
|
||||
<IconButton plain onClick={onErrorClick} label="DETAILS" />
|
||||
<div className="self-center">
|
||||
<IconButton red onClick={onErrorClick} label="DETAILS" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorItem
|
||||
export default ErrorItem;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
.wrapper {
|
||||
border-bottom: solid thin $gray-light-shade;
|
||||
}
|
||||
|
||||
.inactive {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: $teal-light;
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ export default class LinkStyledInput extends React.PureComponent {
|
|||
document.removeEventListener('click', this.onEndChange, false);
|
||||
this.setState({
|
||||
changing: false,
|
||||
value: this.state.value.trim(),
|
||||
value: this.state.value ? this.state.value.trim() : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
0
frontend/app/components/ui/NoContent/NoContent.js
Normal file
0
frontend/app/components/ui/NoContent/NoContent.js
Normal file
|
|
@ -5,21 +5,21 @@ import cn from 'classnames'
|
|||
import { debounce } from 'App/utils';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
interface Props {
|
||||
page: number
|
||||
totalPages: number
|
||||
onPageChange: (page: number) => void
|
||||
limit?: number
|
||||
debounceRequest?: number
|
||||
page: number
|
||||
totalPages: number
|
||||
onPageChange: (page: number) => void
|
||||
limit?: number
|
||||
debounceRequest?: number
|
||||
}
|
||||
export default function Pagination(props: Props) {
|
||||
const { page, totalPages, onPageChange, limit = 5, debounceRequest = 0 } = props;
|
||||
const [currentPage, setCurrentPage] = React.useState(page);
|
||||
React.useMemo(
|
||||
() => setCurrentPage(page),
|
||||
[page],
|
||||
);
|
||||
const { page, totalPages, onPageChange, limit = 5, debounceRequest = 0 } = props;
|
||||
const [currentPage, setCurrentPage] = React.useState(page);
|
||||
React.useMemo(
|
||||
() => setCurrentPage(page),
|
||||
[page],
|
||||
);
|
||||
|
||||
const debounceChange = React.useCallback(debounce(onPageChange, debounceRequest), []);
|
||||
const debounceChange = React.useCallback(debounce(onPageChange, debounceRequest), []);
|
||||
|
||||
const changePage = (page: number) => {
|
||||
if (page > 0 && page <= totalPages) {
|
||||
|
|
@ -33,7 +33,7 @@ export default function Pagination(props: Props) {
|
|||
return (
|
||||
<div className="flex items-center">
|
||||
<Popup
|
||||
content="Previous Page"
|
||||
content="Previous Page"
|
||||
// hideOnClick={true}
|
||||
animation="none"
|
||||
delay={1500}
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Popup } from 'UI';
|
||||
|
||||
export default class Tooltip extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
timeout: 500,
|
||||
}
|
||||
state = {
|
||||
open: false,
|
||||
}
|
||||
mouseOver = false
|
||||
onMouseEnter = () => {
|
||||
this.mouseOver = true;
|
||||
setTimeout(() => {
|
||||
if (this.mouseOver) this.setState({ open: true });
|
||||
}, this.props.timeout)
|
||||
}
|
||||
onMouseLeave = () => {
|
||||
this.mouseOver = false;
|
||||
this.setState({
|
||||
open: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { trigger, tooltip, position } = this.props;
|
||||
const { open } = this.state;
|
||||
return (
|
||||
<Popup
|
||||
open={ open }
|
||||
content={ tooltip }
|
||||
disabled={ !tooltip }
|
||||
position={position}
|
||||
>
|
||||
<span //TODO: no wrap component around
|
||||
onMouseEnter={ this.onMouseEnter }
|
||||
onMouseLeave={ this.onMouseLeave }
|
||||
>
|
||||
{ trigger }
|
||||
</span>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
}
|
||||
51
frontend/app/components/ui/Tooltip/Tooltip.tsx
Normal file
51
frontend/app/components/ui/Tooltip/Tooltip.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import { Popup } from 'UI';
|
||||
|
||||
interface Props {
|
||||
timeout: number
|
||||
position: string
|
||||
tooltip: string
|
||||
trigger: React.ReactNode
|
||||
}
|
||||
|
||||
export default class Tooltip extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
timeout: 500,
|
||||
}
|
||||
state = {
|
||||
open: false,
|
||||
}
|
||||
mouseOver = false
|
||||
onMouseEnter = () => {
|
||||
this.mouseOver = true;
|
||||
setTimeout(() => {
|
||||
if (this.mouseOver) this.setState({ open: true });
|
||||
}, this.props.timeout)
|
||||
}
|
||||
onMouseLeave = () => {
|
||||
this.mouseOver = false;
|
||||
this.setState({
|
||||
open: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { trigger, tooltip, position } = this.props;
|
||||
const { open } = this.state;
|
||||
return (
|
||||
<Popup
|
||||
open={open}
|
||||
content={tooltip}
|
||||
disabled={!tooltip}
|
||||
position={position}
|
||||
>
|
||||
<span //TODO: no wrap component around
|
||||
onMouseEnter={ this.onMouseEnter }
|
||||
onMouseLeave={ this.onMouseLeave }
|
||||
>
|
||||
{ trigger }
|
||||
</span>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
}
|
||||
9
frontend/app/declaration.d.ts
vendored
Normal file
9
frontend/app/declaration.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
declare module '*.scss' {
|
||||
const content: Record<string, string>;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.css' {
|
||||
const content: Record<string, string>;
|
||||
export default content;
|
||||
}
|
||||
|
|
@ -7,15 +7,14 @@ export default class Marker {
|
|||
|
||||
constructor(overlay, screen) {
|
||||
this.screen = screen;
|
||||
|
||||
this._tooltip = document.createElement('div')
|
||||
this._tooltip.className = styles.tooltip;
|
||||
this._tooltip.appendChild(document.createElement('div'))
|
||||
|
||||
const htmlStr = document.createElement('div')
|
||||
htmlStr.innerHTML = "<b>Right-click \> Inspect</b> for more details."
|
||||
this._tooltip.appendChild(htmlStr)
|
||||
|
||||
this._tooltip = document.createElement('div');
|
||||
this._tooltip.className = styles.tooltip;
|
||||
this._tooltip.appendChild(document.createElement('div'));
|
||||
|
||||
const htmlStr = document.createElement('div');
|
||||
htmlStr.innerHTML = '<b>Right-click > Inspect</b> for more details.';
|
||||
this._tooltip.appendChild(htmlStr);
|
||||
|
||||
const marker = document.createElement('div');
|
||||
marker.className = styles.marker;
|
||||
|
|
@ -31,8 +30,8 @@ export default class Marker {
|
|||
marker.appendChild(markerR);
|
||||
marker.appendChild(markerT);
|
||||
marker.appendChild(markerB);
|
||||
|
||||
marker.appendChild(this._tooltip)
|
||||
|
||||
marker.appendChild(this._tooltip);
|
||||
|
||||
overlay.appendChild(marker);
|
||||
this._marker = marker;
|
||||
|
|
@ -55,14 +54,15 @@ export default class Marker {
|
|||
this.mark(null);
|
||||
}
|
||||
|
||||
_autodefineTarget() { // TODO: put to Screen
|
||||
_autodefineTarget() {
|
||||
// TODO: put to Screen
|
||||
if (this._selector) {
|
||||
try {
|
||||
const fitTargets = this.screen.document.querySelectorAll(this._selector);
|
||||
if (fitTargets.length === 0) {
|
||||
this._target = null;
|
||||
} else {
|
||||
this._target = fitTargets[ 0 ];
|
||||
this._target = fitTargets[0];
|
||||
const cursorTarget = this.screen.getCursorTarget();
|
||||
fitTargets.forEach((target) => {
|
||||
if (target.contains(cursorTarget)) {
|
||||
|
|
@ -70,7 +70,7 @@ export default class Marker {
|
|||
}
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
console.info(e);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -85,18 +85,18 @@ export default class Marker {
|
|||
}
|
||||
|
||||
getTagString(tag) {
|
||||
const attrs = tag.attributes
|
||||
let str = `<span style="color:#9BBBDC">${tag.tagName.toLowerCase()}</span>`
|
||||
const attrs = tag.attributes;
|
||||
let str = `<span style="color:#9BBBDC">${tag.tagName.toLowerCase()}</span>`;
|
||||
|
||||
for (let i = 0; i < attrs.length; i++) {
|
||||
let k = attrs[i]
|
||||
const attribute = k.name
|
||||
let k = attrs[i];
|
||||
const attribute = k.name;
|
||||
if (attribute === 'class') {
|
||||
str += `<span style="color:#F29766">${'.' + k.value.split(' ').join('.')}</span>`
|
||||
str += `<span style="color:#F29766">${'.' + k.value.split(' ').join('.')}</span>`;
|
||||
}
|
||||
|
||||
if (attribute === 'id') {
|
||||
str += `<span style="color:#F29766">${'#' + k.value.split(' ').join('#')}</span>`
|
||||
str += `<span style="color:#F29766">${'#' + k.value.split(' ').join('#')}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -117,8 +117,7 @@ export default class Marker {
|
|||
this._marker.style.top = rect.top + 'px';
|
||||
this._marker.style.width = rect.width + 'px';
|
||||
this._marker.style.height = rect.height + 'px';
|
||||
|
||||
|
||||
this._tooltip.firstChild.innerHTML = this.getTagString(this._target);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ export default class DOMManager extends ListWalker<Message> {
|
|||
pNode.sheet &&
|
||||
pNode.sheet.cssRules &&
|
||||
pNode.sheet.cssRules.length > 0 &&
|
||||
pNode.innerText &&
|
||||
pNode.innerText.trim().length === 0
|
||||
) {
|
||||
logger.log("Trying to insert child to a style tag with virtual rules: ", parent, child);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ function cssUrlsIndex(css: string): Array<[number, number]> {
|
|||
return idxs.reverse();
|
||||
}
|
||||
function unquote(str: string): [string, string] {
|
||||
str = str.trim();
|
||||
str = str ? str.trim() : '';
|
||||
if (str.length <= 2) {
|
||||
return [str, ""]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,11 +158,15 @@ export function percentOf(part: number, whole: number): number {
|
|||
return whole > 0 ? (part * 100) / whole : 0;
|
||||
}
|
||||
|
||||
export function fileType(url) {
|
||||
return url.split(/[#?]/)[0].split('.').pop().trim();
|
||||
export function fileType(url: string) {
|
||||
const filename = url.split(/[#?]/)
|
||||
if (!filename || filename.length == 0) return ''
|
||||
const parts = filename[0].split('.')
|
||||
if (!parts || parts.length == 0) return ''
|
||||
return parts.pop().trim();
|
||||
}
|
||||
|
||||
export function fileName(url) {
|
||||
export function fileName(url: string) {
|
||||
if (url) {
|
||||
var m = url.toString().match(/.*\/(.+?)\./);
|
||||
if (m && m.length > 1) {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export function validateName(value, options) {
|
|||
} = Object.assign({}, defaultOptions, options);
|
||||
|
||||
if (typeof value !== 'string') return false; // throw Error?
|
||||
if (!empty && value.trim() === '') return false;
|
||||
if (!empty && value && value.trim() === '') return false;
|
||||
|
||||
const charsRegex = admissibleChars
|
||||
? `|${ admissibleChars.split('').map(escapeRegexp).join('|') }`
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"upload:minio": "node ./scripts/upload-minio.js",
|
||||
"deploy:minio": "yarn build:minio && yarn upload:minio",
|
||||
"lint": "eslint --fix app; exit 0",
|
||||
"tsc": "tsc --noEmit --w --incremental false",
|
||||
"gen:constants": "node ./scripts/constants.js",
|
||||
"gen:icons": "node ./scripts/icons.ts",
|
||||
"gen:colors": "node ./scripts/colors.js",
|
||||
|
|
@ -81,6 +82,7 @@
|
|||
"@babel/preset-typescript": "^7.17.12",
|
||||
"@babel/runtime": "^7.17.9",
|
||||
"@openreplay/sourcemap-uploader": "^3.0.0",
|
||||
"@types/luxon": "^3.0.0",
|
||||
"@types/react": "^18.0.9",
|
||||
"@types/react-dom": "^18.0.4",
|
||||
"@types/react-redux": "^7.1.24",
|
||||
|
|
|
|||
|
|
@ -55,30 +55,30 @@ const config: Configuration = {
|
|||
test: /\.css$/i,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
stylesHandler,
|
||||
{
|
||||
loader: "css-loader",
|
||||
options: {
|
||||
modules: {
|
||||
mode: "local",
|
||||
auto: true,
|
||||
localIdentName: "[name]__[local]--[hash:base64:5]",
|
||||
}
|
||||
// url: {
|
||||
// filter: (url: string) => {
|
||||
// // Semantic-UI-CSS has an extra semi colon in one of the URL due to which CSS loader along
|
||||
// // with webpack 5 fails to generate a build.
|
||||
// // Below if condition is a hack. After Semantic-UI-CSS fixes this, one can replace use clause with just
|
||||
// // use: ['style-loader', 'css-loader']
|
||||
// if (url.includes('charset=utf-8;;')) {
|
||||
// return false;
|
||||
// }
|
||||
// return true;
|
||||
// },
|
||||
// }
|
||||
},
|
||||
stylesHandler,
|
||||
{
|
||||
loader: "css-loader",
|
||||
options: {
|
||||
modules: {
|
||||
mode: "local",
|
||||
auto: true,
|
||||
localIdentName: "[name]__[local]--[hash:base64:5]",
|
||||
}
|
||||
// url: {
|
||||
// filter: (url: string) => {
|
||||
// // Semantic-UI-CSS has an extra semi colon in one of the URL due to which CSS loader along
|
||||
// // with webpack 5 fails to generate a build.
|
||||
// // Below if condition is a hack. After Semantic-UI-CSS fixes this, one can replace use clause with just
|
||||
// // use: ['style-loader', 'css-loader']
|
||||
// if (url.includes('charset=utf-8;;')) {
|
||||
// return false;
|
||||
// }
|
||||
// return true;
|
||||
// },
|
||||
// }
|
||||
},
|
||||
'postcss-loader'
|
||||
},
|
||||
'postcss-loader'
|
||||
],
|
||||
},
|
||||
// {
|
||||
|
|
@ -116,7 +116,7 @@ const config: Configuration = {
|
|||
'window.env.PRODUCTION': isDevelopment ? false : true,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'app/assets/index.html'
|
||||
template: 'app/assets/index.html'
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue