feat(ui): add consistent timestamps to (almost) all items in the player ui
This also tries to make the autoscroll functionality a bit more consistent, where all items are always shown in the list, but items which have not yet occurred will be partially transparent until they happen. Due to that change, autoscroll behavior which previously always went all the way to the bottom of a list didn't make sense anymore, so now it scrolls to the current item.
This commit is contained in:
parent
21fb26b60d
commit
af4160eb20
53 changed files with 2097 additions and 1750 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,4 @@
|
|||
/* padding-right: 10px; */
|
||||
/* border: solid thin $gray-light; */
|
||||
height: 300px;
|
||||
padding-top: 2px;
|
||||
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', {
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,18 @@
|
|||
border-bottom: solid thin $gray-light-shade;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
padding: 7px 0 7px 15px;
|
||||
}
|
||||
|
||||
.activeRow {
|
||||
background-color: $teal !important;
|
||||
color: white !important;
|
||||
background-color: rgba(54, 108, 217, 0.1) !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.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,136 @@
|
|||
import React from 'react';
|
||||
import { NoContent, Input, SlideModal, CloseButton } from 'UI';
|
||||
import { NoContent, Input, SlideModal, CloseButton, Popup, 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';
|
||||
import stl from 'Components/Session_/Network/network.module.css';
|
||||
|
||||
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 { list, listNow, timelinePointer } = this.props;
|
||||
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 +142,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>
|
||||
|
|
|
|||
|
|
@ -95,10 +95,10 @@ export default class Network extends React.PureComponent {
|
|||
};
|
||||
|
||||
onRowClick = (e, index) => {
|
||||
pause();
|
||||
jump(e.time);
|
||||
this.setState({ currentIndex: index });
|
||||
this.props.setTimelinePointer(null);
|
||||
// pause();
|
||||
// jump(e.time);
|
||||
// this.setState({ currentIndex: index });
|
||||
// this.props.setTimelinePointer(null);
|
||||
};
|
||||
|
||||
onTabClick = (activeTab) => this.setState({ activeTab });
|
||||
|
|
@ -108,8 +108,18 @@ export default class Network extends React.PureComponent {
|
|||
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 });
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -137,7 +147,7 @@ export default class Network extends React.PureComponent {
|
|||
resourcesSize={resourcesSize}
|
||||
transferredSize={transferredSize}
|
||||
onRowClick={this.onRowClick}
|
||||
currentIndex={listNow.length - 0}
|
||||
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,318 @@ 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
|
||||
);
|
||||
}
|
||||
|
||||
// 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}>
|
||||
<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>
|
||||
{/* <div className={ stl.location }> */}
|
||||
{/* <Icon name="window" marginRight="8" /> */}
|
||||
{/* <div>{ location }</div> */}
|
||||
{/* <div></div> */}
|
||||
{/* </div> */}
|
||||
<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-3 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;
|
||||
|
|
@ -35,4 +32,12 @@
|
|||
&::-webkit-scrollbar {
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.inactive {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: rgba(54, 108, 217, 0.1);
|
||||
}
|
||||
|
|
@ -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,6 +1,7 @@
|
|||
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';
|
||||
|
|
@ -11,31 +12,33 @@ 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;
|
||||
type Row = 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?: Object;
|
||||
} & RenderOrKey;
|
||||
|
||||
// type RenderOrKey = { // Disjoint?
|
||||
|
|
@ -44,23 +47,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 +83,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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -101,4 +101,8 @@ $offset: 10px;
|
|||
|
||||
.activeRow {
|
||||
background-color: rgba(54, 108, 217, 0.1);
|
||||
}
|
||||
|
||||
.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>
|
||||
|
|
|
|||
|
|
@ -2,19 +2,24 @@ 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-3 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>
|
||||
</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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
.wrapper {
|
||||
border-bottom: solid thin $gray-light-shade;
|
||||
}
|
||||
|
||||
.inactive {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: rgba(54, 108, 217, 0.1);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
function unquote(str: string): [string, string] {
|
||||
str = str.trim();
|
||||
str = str ? str.trim() : '';
|
||||
if (str.length <= 2) {
|
||||
return [str, ""]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
.fill-light-blue-bg { fill: $light-blue-bg }
|
||||
.fill-white { fill: $white }
|
||||
.fill-borderColor { fill: $borderColor }
|
||||
.fill-figmaColors { fill: $figmaColors }
|
||||
|
||||
/* color */
|
||||
.color-main { color: $main }
|
||||
|
|
@ -71,6 +72,7 @@
|
|||
.color-light-blue-bg { color: $light-blue-bg }
|
||||
.color-white { color: $white }
|
||||
.color-borderColor { color: $borderColor }
|
||||
.color-figmaColors { color: $figmaColors }
|
||||
|
||||
/* hover color */
|
||||
.hover-main:hover { color: $main }
|
||||
|
|
@ -107,6 +109,7 @@
|
|||
.hover-light-blue-bg:hover { color: $light-blue-bg }
|
||||
.hover-white:hover { color: $white }
|
||||
.hover-borderColor:hover { color: $borderColor }
|
||||
.hover-figmaColors:hover { color: $figmaColors }
|
||||
|
||||
.border-main { border-color: $main }
|
||||
.border-gray-light-shade { border-color: $gray-light-shade }
|
||||
|
|
@ -142,3 +145,4 @@
|
|||
.border-light-blue-bg { border-color: $light-blue-bg }
|
||||
.border-white { border-color: $white }
|
||||
.border-borderColor { border-color: $borderColor }
|
||||
.border-figmaColors { border-color: $figmaColors }
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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