Merge pull request #679 from Aeolun/feature/more-consistent-player-experience

feat: add consistent timestamps to (almost) all items in the player ui
This commit is contained in:
Delirium 2022-08-23 14:34:22 +01:00 committed by GitHub
commit 103540fb1e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 2199 additions and 1854 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -4,6 +4,6 @@
/* padding-right: 10px; */
/* border: solid thin $gray-light; */
height: 300px;
padding-top: 2px;
border-top: thin dashed #cccccc
border-top: thin dashed #cccccc;
}

View file

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

View file

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

View file

@ -7,6 +7,7 @@ import { LEVEL } from 'Types/session/log';
import Autoscroll from '../Autoscroll';
import BottomBlock from '../BottomBlock';
import stl from './console.module.css';
import { Duration } from 'luxon';
const ALL = 'ALL';
const INFO = 'INFO';
@ -14,108 +15,113 @@ const WARNINGS = 'WARNINGS';
const ERRORS = 'ERRORS';
const LEVEL_TAB = {
[LEVEL.INFO]: INFO,
[LEVEL.LOG]: INFO,
[LEVEL.WARNING]: WARNINGS,
[LEVEL.ERROR]: ERRORS,
[LEVEL.EXCEPTION]: ERRORS,
[LEVEL.INFO]: INFO,
[LEVEL.LOG]: INFO,
[LEVEL.WARNING]: WARNINGS,
[LEVEL.ERROR]: ERRORS,
[LEVEL.EXCEPTION]: ERRORS,
};
const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({ text: tab, key: tab }));
// eslint-disable-next-line complexity
const getIconProps = (level) => {
switch (level) {
case LEVEL.INFO:
case LEVEL.LOG:
return {
name: 'console/info',
color: 'blue2',
};
case LEVEL.WARN:
case LEVEL.WARNING:
return {
name: 'console/warning',
color: 'red2',
};
case LEVEL.ERROR:
return {
name: 'console/error',
color: 'red',
};
}
return null;
switch (level) {
case LEVEL.INFO:
case LEVEL.LOG:
return {
name: 'console/info',
color: 'blue2',
};
case LEVEL.WARN:
case LEVEL.WARNING:
return {
name: 'console/warning',
color: 'red2',
};
case LEVEL.ERROR:
return {
name: 'console/error',
color: 'red',
};
}
return null;
};
function renderWithNL(s = '') {
if (typeof s !== 'string') return '';
return s.split('\n').map((line, i) => <div className={cn({ 'ml-20': i !== 0 })}>{line}</div>);
if (typeof s !== 'string') return '';
return s.split('\n').map((line, i) => <div className={cn({ 'ml-20': i !== 0 })}>{line}</div>);
}
export default class ConsoleContent extends React.PureComponent {
state = {
filter: '',
activeTab: ALL,
};
onTabClick = (activeTab) => this.setState({ activeTab });
onFilterChange = ({ target: { value } }) => this.setState({ filter: value });
state = {
filter: '',
activeTab: ALL,
};
onTabClick = (activeTab) => this.setState({ activeTab });
onFilterChange = ({ target: { value } }) => this.setState({ filter: value });
render() {
const { logs, isResult, additionalHeight, lastIndex } = this.props;
const { filter, activeTab } = this.state;
const filterRE = getRE(filter, 'i');
const filtered = logs.filter(({ level, value }) =>
activeTab === ALL ? filterRE.test(value) : filterRE.test(value) && LEVEL_TAB[level] === activeTab
);
render() {
const { logs, isResult, additionalHeight, logsNow } = this.props;
const time = logsNow.length > 0 ? logsNow[logsNow.length - 1].time : undefined;
const { filter, activeTab, currentError } = this.state;
const filterRE = getRE(filter, 'i');
const filtered = logs.filter(({ level, value }) =>
activeTab === ALL
? filterRE.test(value)
: filterRE.test(value) && LEVEL_TAB[level] === activeTab
);
return (
<>
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }}>
<BottomBlock.Header showClose={!isResult}>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Console</span>
<Tabs tabs={TABS} active={activeTab} onClick={this.onTabClick} border={false} />
</div>
<Input
className="input-small"
placeholder="Filter by keyword"
icon="search"
iconPosition="left"
name="filter"
onChange={this.onFilterChange}
/>
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent
title={<div className="capitalize flex items-center mt-16">
<Icon name="info-circle" className="mr-2" size="18" />
No {activeTab === ALL ? 'Data' : activeTab.toLowerCase()}</div>}
size="small"
show={filtered.length === 0}
>
<Autoscroll>
{filtered.map((l, index) => (
<div
key={l.key}
className={cn(stl.line, {
info: !l.isYellow() && !l.isRed(),
warn: l.isYellow(),
error: l.isRed(),
'cursor-pointer': !isResult,
[stl.activeRow]: lastIndex === index,
})}
data-scroll-item={l.isRed()}
onClick={() => !isResult && jump(l.time)}
>
<Icon size="14" className={stl.icon} {...getIconProps(l.level)} />
<div className={stl.message}>{renderWithNL(l.value)}</div>
</div>
))}
</Autoscroll>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</>
);
}
const lastIndex = filtered.filter((item) => item.time <= time).length - 1;
return (
<>
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }}>
<BottomBlock.Header showClose={!isResult}>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Console</span>
<Tabs tabs={TABS} active={activeTab} onClick={this.onTabClick} border={false} />
</div>
<Input
className="input-small"
placeholder="Filter by keyword"
icon="search"
iconPosition="left"
name="filter"
onChange={this.onFilterChange}
/>
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent size="small" show={filtered.length === 0}>
<Autoscroll autoScrollTo={Math.max(lastIndex, 0)}>
{filtered.map((l, index) => (
<div
className={cn('flex py-2 px-4', {
info: !l.isYellow() && !l.isRed(),
warn: l.isYellow(),
error: l.isRed(),
[stl.activeRow]: lastIndex === index,
[stl.inactiveRow]: index > lastIndex,
'cursor-pointer': !isResult,
})}
onClick={() => !isResult && jump(l.time)}
>
<div className={cn(stl.timestamp)}>
<Icon size="14" className={stl.icon} {...getIconProps(l.level)} />
</div>
<div className={cn(stl.timestamp, {})}>
{Duration.fromMillis(l.time).toFormat('mm:ss.SSS')}
</div>
<div key={l.key} className={cn(stl.line)} data-scroll-item={l.isRed()}>
<div className={stl.message}>{renderWithNL(l.value)}</div>
</div>
</div>
))}
</Autoscroll>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</>
);
}
}

View file

@ -11,18 +11,25 @@
.line {
font-family: 'Menlo', 'monaco', 'consolas', monospace;
padding: 7px 0 7px 15px;
/* margin-top: -1px; ??? */
display: flex;
align-items: flex-start;
border-bottom: solid thin $gray-light-shade;
}
.timestamp {
}
.activeRow {
background-color: $teal !important;
color: white !important;
background-color: $teal-light !important;
}
.icon {
padding-top: 4px;
margin-right: 7px;
}
.inactiveRow {
opacity: 0.5;
}

View file

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

View file

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

View file

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

View file

@ -1,87 +1,134 @@
import React from 'react';
import { NoContent, Input, SlideModal, CloseButton } from 'UI';
import { NoContent, Input, SlideModal, CloseButton, Button } from 'UI';
import { getRE } from 'App/utils';
import { connectPlayer, pause, jump } from 'Player';
import BottomBlock from '../BottomBlock';
import TimeTable from '../TimeTable';
import GQLDetails from './GQLDetails';
import { renderStart } from 'Components/Session_/Network/NetworkContent';
function renderDefaultStatus() {
return "2xx-3xx";
return '2xx-3xx';
}
@connectPlayer(state => ({
list: state.graphqlListNow,
export function renderName(r) {
return (
<div className="flex justify-between items-center grow-0 w-full">
<div>{r.operationName}</div>
<Button
variant="text"
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
onClick={(e) => {
e.stopPropagation();
jump(r.time);
}}
>
Jump
</Button>
</div>
);
}
@connectPlayer((state) => ({
list: state.graphqlList,
listNow: state.graphqlListNow,
time: state.time,
livePlay: state.livePlay,
}))
export default class GraphQL extends React.PureComponent {
state = {
filter: "",
state = {
filter: '',
filteredList: this.props.list,
current: null,
filteredListNow: this.props.listNow,
current: null,
currentIndex: 0,
showFetchDetails: false,
hasNextError: false,
hasPreviousError: false,
}
lastActiveItem: 0,
};
static filterList(list, value) {
const filterRE = getRE(value, 'i');
return value
? list.filter(
(r) =>
filterRE.test(r.operationKind) ||
filterRE.test(r.operationName) ||
filterRE.test(r.variables)
)
: list;
}
onFilterChange = ({ target: { value } }) => {
const { list } = this.props;
const filterRE = getRE(value, 'i');
const filtered = list
.filter((r) => filterRE.test(r.name) || filterRE.test(r.url) || filterRE.test(r.method) || filterRE.test(r.status));
this.setState({ filter: value, filteredList: value ? filtered : list, currentIndex: 0 });
}
const filtered = GraphQL.filterList(list, value);
this.setState({ filter: value, filteredList: filtered, currentIndex: 0 });
};
setCurrent = (item, index) => {
if (!this.props.livePlay) {
pause();
jump(item.time)
jump(item.time);
}
this.setState({ current: item, currentIndex: index });
}
};
closeModal = () => this.setState({ current: null, showFetchDetails: false });
static getDerivedStateFromProps(nextProps, prevState) {
const { filteredList } = prevState;
if (nextProps.timelinePointer) {
let activeItem = filteredList.find((r) => r.time >= nextProps.timelinePointer.time);
activeItem = activeItem || filteredList[filteredList.length - 1];
const { list } = nextProps;
if (nextProps.time) {
const filtered = GraphQL.filterList(list, prevState.filter);
console.log({
list,
filtered,
time: nextProps.time,
});
let i = 0;
filtered.forEach((item, index) => {
if (item.time <= nextProps.time) {
i = index;
}
});
return {
current: activeItem,
currentIndex: filteredList.indexOf(activeItem),
lastActiveItem: i,
};
}
}
render() {
const { list } = this.props;
const { current, currentIndex, filteredList } = this.state;
const { current, currentIndex, filteredList, lastActiveItem } = this.state;
return (
<React.Fragment>
<SlideModal
<SlideModal
size="middle"
right
title = {
title={
<div className="flex justify-between">
<h1>GraphQL</h1>
<div className="flex items-center">
<CloseButton onClick={ this.closeModal } size="18" className="ml-2" />
<CloseButton onClick={this.closeModal} size="18" className="ml-2" />
</div>
</div>
}
isDisplayed={ current != null }
content={ current &&
<GQLDetails
gql={ current }
nextClick={this.nextClickHander}
prevClick={this.prevClickHander}
first={currentIndex === 0}
last={currentIndex === filteredList.length - 1}
/>
isDisplayed={current != null}
content={
current && (
<GQLDetails
gql={current}
nextClick={this.nextClickHander}
prevClick={this.prevClickHander}
first={currentIndex === 0}
last={currentIndex === filteredList.length - 1}
/>
)
}
onClose={ this.closeModal }
onClose={this.closeModal}
/>
<BottomBlock>
<BottomBlock.Header>
@ -93,36 +140,38 @@ export default class GraphQL extends React.PureComponent {
icon="search"
iconPosition="left"
name="filter"
onChange={ this.onFilterChange }
onChange={this.onFilterChange}
/>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent
size="small"
title="No recordings found"
show={ filteredList.length === 0}
>
<NoContent size="small" title="No recordings found" show={filteredList.length === 0}>
<TimeTable
rows={ filteredList }
onRowClick={ this.setCurrent }
rows={filteredList}
onRowClick={this.setCurrent}
hoverable
navigation
activeIndex={currentIndex}
activeIndex={lastActiveItem}
>
{[
{
label: "Status",
label: 'Start',
width: 90,
render: renderStart,
},
{
label: 'Status',
width: 70,
render: renderDefaultStatus,
}, {
label: "Type",
dataKey: "operationKind",
},
{
label: 'Type',
dataKey: 'operationKind',
width: 60,
}, {
label: "Name",
width: 130,
dataKey: "operationName",
},
{
label: 'Name',
width: 240,
render: renderName,
},
]}
</TimeTable>

View file

@ -18,128 +18,141 @@ const MEDIA = 'media';
const OTHER = 'other';
const TAB_TO_TYPE_MAP = {
[XHR]: TYPES.XHR,
[JS]: TYPES.JS,
[CSS]: TYPES.CSS,
[IMG]: TYPES.IMG,
[MEDIA]: TYPES.MEDIA,
[OTHER]: TYPES.OTHER,
[XHR]: TYPES.XHR,
[JS]: TYPES.JS,
[CSS]: TYPES.CSS,
[IMG]: TYPES.IMG,
[MEDIA]: TYPES.MEDIA,
[OTHER]: TYPES.OTHER,
};
export function renderName(r) {
return (
<div className="flex justify-between items-center grow-0 w-full">
<Popup style={{ maxWidth: '75%' }} content={<div className={stl.popupNameContent}>{r.url}</div>}>
<TextEllipsis>{r.name}</TextEllipsis>
</Popup>
<Button
variant="text"
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
onClick={(e) => {
e.stopPropagation();
jump(r.time);
}}
>
Jump
</Button>
</div>
);
return (
<div className="flex justify-between items-center grow-0 w-full">
<Popup
style={{ maxWidth: '75%' }}
content={<div className={stl.popupNameContent}>{r.url}</div>}
>
<TextEllipsis>{r.name}</TextEllipsis>
</Popup>
<Button
variant="text"
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
onClick={(e) => {
e.stopPropagation();
jump(r.time);
}}
>
Jump
</Button>
</div>
);
}
export function renderDuration(r) {
if (!r.success) return 'x';
if (!r.success) return 'x';
const text = `${Math.round(r.duration)}ms`;
if (!r.isRed() && !r.isYellow()) return text;
const text = `${Math.round(r.duration)}ms`;
if (!r.isRed() && !r.isYellow()) return text;
let tooltipText;
let className = 'w-full h-full flex items-center ';
if (r.isYellow()) {
tooltipText = 'Slower than average';
className += 'warn color-orange';
} else {
tooltipText = 'Much slower than average';
className += 'error color-red';
}
let tooltipText;
let className = 'w-full h-full flex items-center ';
if (r.isYellow()) {
tooltipText = 'Slower than average';
className += 'warn color-orange';
} else {
tooltipText = 'Much slower than average';
className += 'error color-red';
}
return (
<Popup content={tooltipText}>
<div className={cn(className, stl.duration)}> {text} </div>
</Popup>
);
return (
<Popup content={tooltipText}>
<div className={cn(className, stl.duration)}> {text} </div>
</Popup>
);
}
@connectPlayer((state) => ({
location: state.location,
resources: state.resourceList,
domContentLoadedTime: state.domContentLoadedTime,
loadTime: state.loadTime,
// time: state.time,
playing: state.playing,
domBuildingTime: state.domBuildingTime,
fetchPresented: state.fetchList.length > 0,
listNow: state.resourceListNow,
location: state.location,
resources: state.resourceList,
domContentLoadedTime: state.domContentLoadedTime,
loadTime: state.loadTime,
// time: state.time,
playing: state.playing,
domBuildingTime: state.domBuildingTime,
fetchPresented: state.fetchList.length > 0,
listNow: state.resourceListNow,
}))
@connect(
(state) => ({
timelinePointer: state.getIn(['sessions', 'timelinePointer']),
}),
{ setTimelinePointer }
(state) => ({
timelinePointer: state.getIn(['sessions', 'timelinePointer']),
}),
{ setTimelinePointer }
)
export default class Network extends React.PureComponent {
state = {
filter: '',
filteredList: this.props.resources,
activeTab: ALL,
currentIndex: 0,
};
state = {
filter: '',
filteredList: this.props.resources,
activeTab: ALL,
currentIndex: 0,
};
onRowClick = (e, index) => {
pause();
jump(e.time);
this.setState({ currentIndex: index });
this.props.setTimelinePointer(null);
};
onRowClick = (e, index) => {
// no action for direct click on network requests (so far), there is a jump button, and we don't have more information for than is already displayed in the table
};
onTabClick = (activeTab) => this.setState({ activeTab });
onTabClick = (activeTab) => this.setState({ activeTab });
onFilterChange = (e, { value }) => {
const { resources } = this.props;
const filterRE = getRE(value, 'i');
const filtered = resources.filter(({ type, name }) => filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab]));
onFilterChange = (e, { value }) => {
const { resources } = this.props;
const filterRE = getRE(value, 'i');
const filtered = resources.filter(
({ type, name }) =>
filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab])
);
this.setState({ filter: value, filteredList: value ? filtered : resources, currentIndex: 0 });
};
this.setState({ filter: value, filteredList: value ? filtered : resources, currentIndex: 0 });
};
render() {
const {
location,
domContentLoadedTime,
loadTime,
domBuildingTime,
fetchPresented,
listNow,
} = this.props;
const { filteredList } = this.state;
const resourcesSize = filteredList.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0);
const transferredSize = filteredList.reduce((sum, { headerSize, encodedBodySize }) => sum + (headerSize || 0) + (encodedBodySize || 0), 0);
return (
<React.Fragment>
<NetworkContent
// time = { time }
location={location}
resources={filteredList}
domContentLoadedTime={domContentLoadedTime}
loadTime={loadTime}
domBuildingTime={domBuildingTime}
fetchPresented={fetchPresented}
resourcesSize={resourcesSize}
transferredSize={transferredSize}
onRowClick={this.onRowClick}
currentIndex={listNow.length - 0}
/>
</React.Fragment>
);
static getDerivedStateFromProps(nextProps, prevState) {
const { filteredList } = prevState;
if (nextProps.timelinePointer) {
const activeItem = filteredList.find((r) => r.time >= nextProps.timelinePointer.time);
return {
currentIndex: activeItem ? filteredList.indexOf(activeItem) : filteredList.length - 1,
};
}
}
render() {
const { location, domContentLoadedTime, loadTime, domBuildingTime, fetchPresented, listNow } =
this.props;
const { filteredList } = this.state;
const resourcesSize = filteredList.reduce(
(sum, { decodedBodySize }) => sum + (decodedBodySize || 0),
0
);
const transferredSize = filteredList.reduce(
(sum, { headerSize, encodedBodySize }) => sum + (headerSize || 0) + (encodedBodySize || 0),
0
);
return (
<React.Fragment>
<NetworkContent
// time = { time }
location={location}
resources={filteredList}
domContentLoadedTime={domContentLoadedTime}
loadTime={loadTime}
domBuildingTime={domBuildingTime}
fetchPresented={fetchPresented}
resourcesSize={resourcesSize}
transferredSize={transferredSize}
onRowClick={this.onRowClick}
currentIndex={listNow.length - 1}
/>
</React.Fragment>
);
}
}

View file

@ -1,7 +1,7 @@
import React from 'react';
import cn from 'classnames';
// import { connectPlayer } from 'Player';
import { QuestionMarkHint, Popup, Tabs, Input, NoContent, Icon } from 'UI';
import { QuestionMarkHint, Popup, Tabs, Input, NoContent, Icon, Button } from 'UI';
import { getRE } from 'App/utils';
import { TYPES } from 'Types/session/resource';
import { formatBytes } from 'App/utils';
@ -11,6 +11,8 @@ import TimeTable from '../TimeTable';
import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
import stl from './network.module.css';
import { Duration } from 'luxon';
import { jump } from 'Player';
const ALL = 'ALL';
const XHR = 'xhr';
@ -21,261 +23,309 @@ const MEDIA = 'media';
const OTHER = 'other';
const TAB_TO_TYPE_MAP = {
[XHR]: TYPES.XHR,
[JS]: TYPES.JS,
[CSS]: TYPES.CSS,
[IMG]: TYPES.IMG,
[MEDIA]: TYPES.MEDIA,
[OTHER]: TYPES.OTHER,
[XHR]: TYPES.XHR,
[JS]: TYPES.JS,
[CSS]: TYPES.CSS,
[IMG]: TYPES.IMG,
[MEDIA]: TYPES.MEDIA,
[OTHER]: TYPES.OTHER,
};
const TABS = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER].map((tab) => ({
text: tab,
key: tab,
text: tab,
key: tab,
}));
const DOM_LOADED_TIME_COLOR = 'teal';
const LOAD_TIME_COLOR = 'red';
export function renderType(r) {
return (
<Popup style={{ width: '100%' }} content={<div className={stl.popupNameContent}>{r.type}</div>}>
<div className={stl.popupNameTrigger}>{r.type}</div>
</Popup>
);
return (
<Popup style={{ width: '100%' }} content={<div className={stl.popupNameContent}>{r.type}</div>}>
<div className={stl.popupNameTrigger}>{r.type}</div>
</Popup>
);
}
export function renderName(r) {
return (
<Popup style={{ width: '100%' }} content={<div className={stl.popupNameContent}>{r.url}</div>}>
<div className={stl.popupNameTrigger}>{r.name}</div>
</Popup>
);
return (
<div className="flex justify-between items-center grow-0 w-full">
<Popup
style={{ width: '100%' }}
content={<div className={stl.popupNameContent}>{r.url}</div>}
>
<div className={stl.popupNameTrigger}>{r.name}</div>
</Popup>
<Button
variant="text"
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
onClick={(e) => {
e.stopPropagation();
jump(r.time);
}}
>
Jump
</Button>
</div>
);
}
export function renderStart(r) {
return Duration.fromMillis(r.time).toFormat('mm:ss.SSS');
}
const renderXHRText = () => (
<span className="flex items-center">
{XHR}
<QuestionMarkHint
onHover={true}
content={
<>
Use our{' '}
<a className="color-teal underline" target="_blank" href="https://docs.openreplay.com/plugins/fetch">
Fetch plugin
</a>
{' to capture HTTP requests and responses, including status codes and bodies.'} <br />
We also provide{' '}
<a className="color-teal underline" target="_blank" href="https://docs.openreplay.com/plugins/graphql">
support for GraphQL
</a>
{' for easy debugging of your queries.'}
</>
}
className="ml-1"
/>
</span>
<span className="flex items-center">
{XHR}
<QuestionMarkHint
onHover={true}
content={
<>
Use our{' '}
<a
className="color-teal underline"
target="_blank"
href="https://docs.openreplay.com/plugins/fetch"
>
Fetch plugin
</a>
{' to capture HTTP requests and responses, including status codes and bodies.'} <br />
We also provide{' '}
<a
className="color-teal underline"
target="_blank"
href="https://docs.openreplay.com/plugins/graphql"
>
support for GraphQL
</a>
{' for easy debugging of your queries.'}
</>
}
className="ml-1"
/>
</span>
);
function renderSize(r) {
if (r.responseBodySize) return formatBytes(r.responseBodySize);
let triggerText;
let content;
if (r.decodedBodySize == null) {
triggerText = 'x';
content = 'Not captured';
} else {
const headerSize = r.headerSize || 0;
const encodedSize = r.encodedBodySize || 0;
const transferred = headerSize + encodedSize;
const showTransferred = r.headerSize != null;
if (r.responseBodySize) return formatBytes(r.responseBodySize);
let triggerText;
let content;
if (r.decodedBodySize == null) {
triggerText = 'x';
content = 'Not captured';
} else {
const headerSize = r.headerSize || 0;
const encodedSize = r.encodedBodySize || 0;
const transferred = headerSize + encodedSize;
const showTransferred = r.headerSize != null;
triggerText = formatBytes(r.decodedBodySize);
content = (
<ul>
{showTransferred && <li>{`${formatBytes(r.encodedBodySize + headerSize)} transfered over network`}</li>}
<li>{`Resource size: ${formatBytes(r.decodedBodySize)} `}</li>
</ul>
);
}
return (
<Popup style={{ width: '100%' }} content={content}>
<div>{triggerText}</div>
</Popup>
triggerText = formatBytes(r.decodedBodySize);
content = (
<ul>
{showTransferred && (
<li>{`${formatBytes(r.encodedBodySize + headerSize)} transfered over network`}</li>
)}
<li>{`Resource size: ${formatBytes(r.decodedBodySize)} `}</li>
</ul>
);
}
return (
<Popup style={{ width: '100%' }} content={content}>
<div>{triggerText}</div>
</Popup>
);
}
export function renderDuration(r) {
if (!r.success) return 'x';
if (!r.success) return 'x';
const text = `${Math.floor(r.duration)}ms`;
if (!r.isRed() && !r.isYellow()) return text;
const text = `${Math.floor(r.duration)}ms`;
if (!r.isRed() && !r.isYellow()) return text;
let tooltipText;
let className = 'w-full h-full flex items-center ';
if (r.isYellow()) {
tooltipText = 'Slower than average';
className += 'warn color-orange';
} else {
tooltipText = 'Much slower than average';
className += 'error color-red';
}
let tooltipText;
let className = 'w-full h-full flex items-center ';
if (r.isYellow()) {
tooltipText = 'Slower than average';
className += 'warn color-orange';
} else {
tooltipText = 'Much slower than average';
className += 'error color-red';
}
return (
<Popup style={{ width: '100%' }} content={tooltipText}>
<div className={cn(className, stl.duration)}> {text} </div>
</Popup>
);
return (
<Popup style={{ width: '100%' }} content={tooltipText}>
<div className={cn(className, stl.duration)}> {text} </div>
</Popup>
);
}
export default class NetworkContent extends React.PureComponent {
state = {
filter: '',
activeTab: ALL,
};
state = {
filter: '',
activeTab: ALL,
};
onTabClick = (activeTab) => this.setState({ activeTab });
onFilterChange = ({ target: { value } }) => this.setState({ filter: value });
onTabClick = (activeTab) => this.setState({ activeTab });
onFilterChange = ({ target: { value } }) => this.setState({ filter: value });
render() {
const {
location,
resources,
domContentLoadedTime,
loadTime,
domBuildingTime,
fetchPresented,
onRowClick,
isResult = false,
additionalHeight = 0,
resourcesSize,
transferredSize,
time,
currentIndex,
} = this.props;
const { filter, activeTab } = this.state;
const filterRE = getRE(filter, 'i');
let filtered = resources.filter(({ type, name }) => filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab]));
const lastIndex = currentIndex || filtered.filter((item) => item.time <= time).length - 1;
render() {
const {
location,
resources,
domContentLoadedTime,
loadTime,
domBuildingTime,
fetchPresented,
onRowClick,
isResult = false,
additionalHeight = 0,
resourcesSize,
transferredSize,
time,
currentIndex,
} = this.props;
const { filter, activeTab } = this.state;
const filterRE = getRE(filter, 'i');
let filtered = resources.filter(
({ type, name }) =>
filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab])
);
const lastIndex = currentIndex || filtered.filter((item) => item.time <= time).length - 1;
const referenceLines = [];
if (domContentLoadedTime != null) {
referenceLines.push({
time: domContentLoadedTime.time,
color: DOM_LOADED_TIME_COLOR,
});
}
if (loadTime != null) {
referenceLines.push({
time: loadTime.time,
color: LOAD_TIME_COLOR,
});
}
let tabs = TABS;
if (!fetchPresented) {
tabs = TABS.map((tab) =>
!isResult && tab.key === XHR
? {
text: renderXHRText(),
key: XHR,
}
: tab
);
}
// const resourcesSize = filtered.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0);
// const transferredSize = filtered
// .reduce((sum, { headerSize, encodedBodySize }) => sum + (headerSize || 0) + (encodedBodySize || 0), 0);
return (
<React.Fragment>
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }} className="border">
<BottomBlock.Header showClose={!isResult}>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Network</span>
<Tabs className="uppercase" tabs={tabs} active={activeTab} onClick={this.onTabClick} border={false} />
</div>
<Input
// className="input-small"
placeholder="Filter by Name"
icon="search"
iconPosition="left"
name="filter"
onChange={this.onFilterChange}
/>
</BottomBlock.Header>
<BottomBlock.Content>
<InfoLine>
<InfoLine.Point label={filtered.length} value=" requests" />
<InfoLine.Point label={formatBytes(transferredSize)} value="transferred" display={transferredSize > 0} />
<InfoLine.Point label={formatBytes(resourcesSize)} value="resources" display={resourcesSize > 0} />
<InfoLine.Point label="DOM Building Time" value={formatMs(domBuildingTime)} display={domBuildingTime != null} />
<InfoLine.Point
label="DOMContentLoaded"
value={domContentLoadedTime && formatMs(domContentLoadedTime.value)}
display={domContentLoadedTime != null}
dotColor={DOM_LOADED_TIME_COLOR}
/>
<InfoLine.Point
label="Load"
value={loadTime && formatMs(loadTime.value)}
display={loadTime != null}
dotColor={LOAD_TIME_COLOR}
/>
</InfoLine>
<NoContent
title={
<div className="capitalize flex items-center mt-16">
<Icon name="info-circle" className="mr-2" size="18" />
No Data
</div>
}
size="small"
show={filtered.length === 0}
>
<TimeTable
rows={filtered}
referenceLines={referenceLines}
renderPopup
// navigation
onRowClick={onRowClick}
additionalHeight={additionalHeight}
activeIndex={lastIndex}
>
{[
{
label: 'Status',
dataKey: 'status',
width: 70,
},
{
label: 'Type',
dataKey: 'type',
width: 90,
render: renderType,
},
{
label: 'Name',
width: 200,
render: renderName,
},
{
label: 'Size',
width: 60,
render: renderSize,
},
{
label: 'Time',
width: 80,
render: renderDuration,
},
]}
</TimeTable>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</React.Fragment>
);
const referenceLines = [];
if (domContentLoadedTime != null) {
referenceLines.push({
time: domContentLoadedTime.time,
color: DOM_LOADED_TIME_COLOR,
});
}
if (loadTime != null) {
referenceLines.push({
time: loadTime.time,
color: LOAD_TIME_COLOR,
});
}
let tabs = TABS;
if (!fetchPresented) {
tabs = TABS.map((tab) =>
!isResult && tab.key === XHR
? {
text: renderXHRText(),
key: XHR,
}
: tab
);
}
return (
<React.Fragment>
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }} className="border">
<BottomBlock.Header showClose={!isResult}>
<Tabs
className="uppercase"
tabs={tabs}
active={activeTab}
onClick={this.onTabClick}
border={false}
/>
<Input
className="input-small"
placeholder="Filter by Name"
icon="search"
iconPosition="left"
name="filter"
onChange={this.onFilterChange}
/>
</BottomBlock.Header>
<BottomBlock.Content>
<InfoLine>
<InfoLine.Point label={filtered.length} value=" requests" />
<InfoLine.Point
label={formatBytes(transferredSize)}
value="transferred"
display={transferredSize > 0}
/>
<InfoLine.Point
label={formatBytes(resourcesSize)}
value="resources"
display={resourcesSize > 0}
/>
<InfoLine.Point
label={formatMs(domBuildingTime)}
value="DOM Building Time"
display={domBuildingTime != null}
/>
<InfoLine.Point
label={domContentLoadedTime && formatMs(domContentLoadedTime.value)}
value="DOMContentLoaded"
display={domContentLoadedTime != null}
dotColor={DOM_LOADED_TIME_COLOR}
/>
<InfoLine.Point
label={loadTime && formatMs(loadTime.value)}
value="Load"
display={loadTime != null}
dotColor={LOAD_TIME_COLOR}
/>
</InfoLine>
<NoContent
title={
<div className="capitalize flex items-center mt-16">
<Icon name="info-circle" className="mr-2" size="18" />
No Data
</div>
}
size="small"
show={filtered.length === 0}
>
<TimeTable
rows={filtered}
referenceLines={referenceLines}
renderPopup
// navigation
onRowClick={onRowClick}
additionalHeight={additionalHeight}
activeIndex={lastIndex}
>
{[
{
label: 'Start',
width: 90,
render: renderStart,
},
{
label: 'Status',
dataKey: 'status',
width: 70,
},
{
label: 'Type',
dataKey: 'type',
width: 90,
render: renderType,
},
{
label: 'Name',
width: 240,
render: renderName,
},
{
label: 'Size',
width: 60,
render: renderSize,
},
{
label: 'Time',
width: 80,
render: renderDuration,
},
]}
</TimeTable>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</React.Fragment>
);
}
}

View file

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

View file

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

View file

@ -1,122 +1,68 @@
import React from 'react';
import cn from 'classnames';
import { OPENREPLAY, SENTRY, DATADOG, STACKDRIVER } from 'Types/session/stackEvent';
import { Icon, SlideModal, IconButton } from 'UI';
import { OPENREPLAY, SENTRY, DATADOG, STACKDRIVER } from 'Types/session/stackEvent';
import { Icon, IconButton } from 'UI';
import withToggle from 'HOCs/withToggle';
import Sentry from './Sentry';
import JsonViewer from './JsonViewer';
import stl from './userEvent.module.css';
import { Duration } from 'luxon';
// const modalSources = [ SENTRY, DATADOG ];
@withToggle() //
@withToggle() //
export default class UserEvent extends React.PureComponent {
getIconProps() {
const { source } = this.props.userEvent;
return {
name: `integrations/${ source }`,
size: 18,
marginRight: source === OPENREPLAY ? 11 : 10
}
}
getIconProps() {
const { source } = this.props.userEvent;
return {
name: `integrations/${source}`,
size: 18,
marginRight: source === OPENREPLAY ? 11 : 10,
};
}
getLevelClassname() {
const { userEvent } = this.props;
if (userEvent.isRed()) return "error color-red";
return '';
}
getLevelClassname() {
const { userEvent } = this.props;
if (userEvent.isRed()) return 'error color-red';
return '';
}
// getEventMessage() {
// const { userEvent } = this.props;
// switch(userEvent.source) {
// case SENTRY:
// case DATADOG:
// return null;
// default:
// return JSON.stringify(userEvent.data);
// }
// }
onClickDetails = (e) => {
e.stopPropagation();
this.props.onDetailsClick(this.props.userEvent);
};
renderPopupContent() {
const { userEvent: { source, payload, name} } = this.props;
switch(source) {
case SENTRY:
return <Sentry event={ payload } />;
case DATADOG:
return <JsonViewer title={ name } data={ payload } icon="integrations/datadog" />;
case STACKDRIVER:
return <JsonViewer title={ name } data={ payload } icon="integrations/stackdriver" />;
default:
return <JsonViewer title={ name } data={ payload } icon={ `integrations/${ source }` } />;
}
}
ifNeedModal() {
return !!this.props.userEvent.payload;
}
onClickDetails = (e) => {
e.stopPropagation();
this.props.switchOpen();
}
renderContent(modalTrigger) {
const { userEvent } = this.props;
//const message = this.getEventMessage();
return (
<div
data-scroll-item={ userEvent.isRed() }
// onClick={ this.props.switchOpen } //
onClick={ this.props.onJump } //
className={
cn(
"group",
stl.userEvent,
this.getLevelClassname(),
{ [ stl.modalTrigger ]: modalTrigger }
)
}
>
<div className={ stl.infoWrapper }>
<div className={ stl.title } >
<Icon { ...this.getIconProps() } />
{ userEvent.name }
</div>
{ /* message &&
<div className={ stl.message }>
{ message }
</div> */
}
<div className="invisible self-end ml-auto group-hover:visible">
<IconButton size="small" plain onClick={this.onClickDetails} label="DETAILS" />
</div>
</div>
</div>
);
}
render() {
const { userEvent } = this.props;
if (this.ifNeedModal()) {
return (
<React.Fragment>
<SlideModal
//title="Add Custom Field"
size="middle"
isDisplayed={ this.props.open }
content={ this.props.open && this.renderPopupContent() }
onClose={ this.props.switchOpen }
/>
{ this.renderContent(true) }
</React.Fragment>
//<Modal
// trigger={ this.renderContent(true) }
// content={ this.renderPopupContent() }
// centered={ false }
// size="small"
// />
);
}
return this.renderContent();
}
render() {
const { userEvent, inactive, selected } = this.props;
//const message = this.getEventMessage();
return (
<div
data-scroll-item={userEvent.isRed()}
// onClick={ this.props.switchOpen } //
onClick={this.props.onJump} //
className={cn('group flex py-2 px-4 ', stl.userEvent, this.getLevelClassname(), {
[stl.inactive]: inactive,
[stl.selected]: selected,
})}
>
<div className={'self-start pr-4'}>
{Duration.fromMillis(userEvent.time).toFormat('mm:ss.SSS')}
</div>
<div className={cn('mr-auto', stl.infoWrapper)}>
<div className={stl.title}>
<Icon {...this.getIconProps()} />
{userEvent.name}
</div>
</div>
<div className="self-center">
<IconButton
outline={!userEvent.isRed()}
red={userEvent.isRed()}
onClick={this.onClickDetails}
label="DETAILS"
/>
</div>
</div>
);
}
}

View file

@ -2,9 +2,6 @@
.userEvent {
border-radius: 3px;
background-color: rgba(0, 118, 255, 0.05);
font-family: 'Menlo', 'monaco', 'consolas', monospace;
padding: 8px 10px;
margin: 3px 0;
&.modalTrigger {
cursor: pointer;
@ -15,6 +12,7 @@
overflow: hidden;
display: flex;
align-items: flex-start;
font-family: 'Menlo', 'monaco', 'consolas', monospace;
}
.title {
@ -35,4 +33,12 @@
&::-webkit-scrollbar {
height: 1px;
}
}
.inactive {
opacity: 0.5;
}
.selected {
background-color: $teal-light;
}

View file

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

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

View file

@ -1,9 +1,9 @@
import React from 'react';
import { List, AutoSizer } from 'react-virtualized';
import cn from 'classnames';
import { Duration } from "luxon";
import { NoContent, IconButton, Button } from 'UI';
import { percentOf } from 'App/utils';
import { formatMs } from 'App/date';
import BarRow from './BarRow';
import stl from './timeTable.module.css';
@ -11,31 +11,35 @@ import stl from './timeTable.module.css';
import autoscrollStl from '../autoscroll.module.css'; //aaa
type Timed = {
time: number;
time: number;
};
type Durationed = {
duration: number;
duration: number;
};
type CanBeRed = {
//+isRed: boolean,
isRed: () => boolean;
//+isRed: boolean,
isRed: () => boolean;
};
type Row = Timed & Durationed & CanBeRed;
interface Row extends Timed, Durationed, CanBeRed {
[key: string]: any, key: string
}
type Line = {
color: string; // Maybe use typescript?
hint?: string;
onClick?: any;
color: string; // Maybe use typescript?
hint?: string;
onClick?: any;
} & Timed;
type Column = {
label: string;
width: number;
referenceLines?: Array<Line>;
style?: Object;
label: string;
width: number;
dataKey?: string;
render?: (row: any) => void
referenceLines?: Array<Line>;
style?: React.CSSProperties;
} & RenderOrKey;
// type RenderOrKey = { // Disjoint?
@ -44,23 +48,31 @@ type Column = {
// dataKey: string,
// }
type RenderOrKey =
| {
render?: (row: Row) => React.ReactNode;
key?: string;
}
| {
dataKey: string;
};
| {
render?: (row: Row) => React.ReactNode;
key?: string;
}
| {
dataKey: string;
};
type Props = {
className?: string;
rows: Array<Row>;
children: Array<Column>;
className?: string;
rows: Array<Row>;
children: Array<Column>;
tableHeight?: number
activeIndex?: number
renderPopup?: boolean
navigation?: boolean
referenceLines?: any[]
additionalHeight?: number
hoverable?: boolean
onRowClick?: (row: any, index: number) => void
};
type TimeLineInfo = {
timestart: number;
timewidth: number;
timestart: number;
timewidth: number;
};
type State = TimeLineInfo & typeof initialState;
@ -72,247 +84,235 @@ const ROW_HEIGHT = 32;
const TIME_SECTIONS_COUNT = 8;
const ZERO_TIMEWIDTH = 1000;
function formatTime(ms) {
if (ms < 0) return '';
return formatMs(ms);
function formatTime(ms: number) {
if (ms < 0) return '';
if (ms < 1000) return Duration.fromMillis(ms).toFormat('0.SSS')
return Duration.fromMillis(ms).toFormat('mm:ss');
}
function computeTimeLine(rows: Array<Row>, firstVisibleRowIndex: number, visibleCount): TimeLineInfo {
const visibleRows = rows.slice(firstVisibleRowIndex, firstVisibleRowIndex + visibleCount + _additionalHeight);
let timestart = visibleRows.length > 0 ? Math.min(...visibleRows.map((r) => r.time)) : 0;
const timeend = visibleRows.length > 0 ? Math.max(...visibleRows.map((r) => r.time + r.duration)) : 0;
let timewidth = timeend - timestart;
const offset = timewidth / 70;
if (timestart >= offset) {
timestart -= offset;
}
timewidth *= 1.5; // += offset;
if (timewidth === 0) {
timewidth = ZERO_TIMEWIDTH;
}
return {
timestart,
timewidth,
};
function computeTimeLine(rows: Array<Row>, firstVisibleRowIndex: number, visibleCount: number): TimeLineInfo {
const visibleRows = rows.slice(firstVisibleRowIndex, firstVisibleRowIndex + visibleCount + _additionalHeight);
let timestart = visibleRows.length > 0 ? Math.min(...visibleRows.map((r) => r.time)) : 0;
// TODO: GraphQL requests do not have a duration, so their timeline is borked. Assume a duration of 0.2s for every GraphQL request
const timeend = visibleRows.length > 0 ? Math.max(...visibleRows.map((r) => r.time + (r.duration ?? 200))) : 0;
let timewidth = timeend - timestart;
const offset = timewidth / 70;
if (timestart >= offset) {
timestart -= offset;
}
timewidth *= 1.5; // += offset;
if (timewidth === 0) {
timewidth = ZERO_TIMEWIDTH;
}
return {
timestart,
timewidth,
};
}
const initialState = {
firstVisibleRowIndex: 0,
firstVisibleRowIndex: 0,
};
export default class TimeTable extends React.PureComponent<Props, State> {
state = {
...computeTimeLine(this.props.rows, initialState.firstVisibleRowIndex, this.visibleCount),
...initialState,
};
state = {
...computeTimeLine(this.props.rows, initialState.firstVisibleRowIndex, this.visibleCount),
...initialState,
};
get tableHeight() {
return this.props.tableHeight || 195;
get tableHeight() {
return this.props.tableHeight || 195;
}
get visibleCount() {
return Math.ceil(this.tableHeight / ROW_HEIGHT);
}
scroller = React.createRef<List>();
autoScroll = true;
componentDidMount() {
if (this.scroller.current) {
this.scroller.current.scrollToRow(this.props.activeIndex);
}
}
componentDidUpdate(prevProps: any, prevState: any) {
if (
prevState.firstVisibleRowIndex !== this.state.firstVisibleRowIndex ||
(this.props.rows.length <= this.visibleCount + _additionalHeight && prevProps.rows.length !== this.props.rows.length)
) {
this.setState({
...computeTimeLine(this.props.rows, this.state.firstVisibleRowIndex, this.visibleCount),
});
}
if (this.props.activeIndex && this.props.activeIndex >= 0 && prevProps.activeIndex !== this.props.activeIndex && this.scroller.current) {
this.scroller.current.scrollToRow(this.props.activeIndex);
}
}
onScroll = ({ scrollTop, scrollHeight, clientHeight }: { scrollTop: number; scrollHeight: number; clientHeight: number }): void => {
const firstVisibleRowIndex = Math.floor(scrollTop / ROW_HEIGHT + 0.33);
if (this.state.firstVisibleRowIndex !== firstVisibleRowIndex) {
this.autoScroll = scrollHeight - clientHeight - scrollTop < ROW_HEIGHT / 2;
this.setState({ firstVisibleRowIndex });
}
};
renderRow = ({ index, key, style: rowStyle }: any) => {
const { activeIndex } = this.props;
const { children: columns, rows, renderPopup, hoverable, onRowClick } = this.props;
const { timestart, timewidth } = this.state;
const row = rows[index];
return (
<div
style={rowStyle}
key={key}
className={cn('border-b border-color-gray-light-shade', stl.row, {
[stl.hoverable]: hoverable,
'error color-red': !!row.isRed && row.isRed(),
'cursor-pointer': typeof onRowClick === 'function',
[stl.activeRow]: activeIndex === index,
[stl.inactiveRow]: !activeIndex || index > activeIndex,
})}
onClick={typeof onRowClick === 'function' ? () => onRowClick(row, index) : undefined}
id="table-row"
>
{columns.map(({ dataKey, render, width }) => (
<div className={stl.cell} style={{ width: `${width}px` }}>
{render ? render(row) : row[dataKey || ''] || <i className="color-gray-light">{'empty'}</i>}
</div>
))}
<div className={cn('relative flex-1 flex', stl.timeBarWrapper)}>
<BarRow resource={row} timestart={timestart} timewidth={timewidth} popup={renderPopup} />
</div>
</div>
);
};
onPrevClick = () => {
let prevRedIndex = -1;
for (let i = this.state.firstVisibleRowIndex - 1; i >= 0; i--) {
if (this.props.rows[i].isRed()) {
prevRedIndex = i;
break;
}
}
if (this.scroller.current != null) {
this.scroller.current.scrollToRow(prevRedIndex);
}
};
onNextClick = () => {
let prevRedIndex = -1;
for (let i = this.state.firstVisibleRowIndex + 1; i < this.props.rows.length; i++) {
if (this.props.rows[i].isRed()) {
prevRedIndex = i;
break;
}
}
if (this.scroller.current != null) {
this.scroller.current.scrollToRow(prevRedIndex);
}
};
render() {
const { className, rows, children: columns, navigation = false, referenceLines = [], additionalHeight = 0, activeIndex } = this.props;
const { timewidth, timestart } = this.state;
_additionalHeight = additionalHeight;
const sectionDuration = Math.round(timewidth / TIME_SECTIONS_COUNT);
const timeColumns: number[] = [];
if (timewidth > 0) {
for (let i = 0; i < TIME_SECTIONS_COUNT; i++) {
timeColumns.push(timestart + i * sectionDuration);
}
}
get visibleCount() {
return Math.ceil(this.tableHeight / ROW_HEIGHT);
}
const visibleRefLines = referenceLines.filter(({ time }) => time > timestart && time < timestart + timewidth);
scroller = React.createRef();
autoScroll = true;
const columnsSumWidth = columns.reduce((sum, { width }) => sum + width, 0);
componentDidMount() {
if (this.scroller.current) {
this.scroller.current.scrollToRow(this.props.activeIndex);
}
}
componentDidUpdate(prevProps: any, prevState: any) {
// if (prevProps.rows.length !== this.props.rows.length &&
// this.autoScroll &&
// this.scroller.current != null) {
// this.scroller.current.scrollToRow(this.props.rows.length);
// }
if (
prevState.firstVisibleRowIndex !== this.state.firstVisibleRowIndex ||
(this.props.rows.length <= this.visibleCount + _additionalHeight && prevProps.rows.length !== this.props.rows.length)
) {
this.setState({
...computeTimeLine(this.props.rows, this.state.firstVisibleRowIndex, this.visibleCount),
});
}
if (this.props.activeIndex >= 0 && prevProps.activeIndex !== this.props.activeIndex && this.scroller.current) {
this.scroller.current.scrollToRow(this.props.activeIndex);
}
}
onScroll = ({ scrollTop, scrollHeight, clientHeight }: { scrollTop: number; scrollHeight: number; clientHeight: number }): void => {
const firstVisibleRowIndex = Math.floor(scrollTop / ROW_HEIGHT + 0.33);
if (this.state.firstVisibleRowIndex !== firstVisibleRowIndex) {
this.autoScroll = scrollHeight - clientHeight - scrollTop < ROW_HEIGHT / 2;
this.setState({ firstVisibleRowIndex });
}
};
renderRow = ({ index, key, style: rowStyle }: any) => {
const { activeIndex } = this.props;
const { children: columns, rows, renderPopup, hoverable, onRowClick } = this.props;
const { timestart, timewidth } = this.state;
const row = rows[index];
return (
<div
style={rowStyle}
key={key}
className={cn('border-b border-color-gray-light-shade', stl.row, {
[stl.hoverable]: hoverable,
'error color-red': !!row.isRed && row.isRed(),
'cursor-pointer': typeof onRowClick === 'function',
[stl.activeRow]: activeIndex === index,
})}
onClick={typeof onRowClick === 'function' ? () => onRowClick(row, index) : null}
id="table-row"
>
{columns.map(({ dataKey, render, width }) => (
<div className={stl.cell} style={{ width: `${width}px` }}>
{render ? render(row) : row[dataKey] || <i className="color-gray-light">{'empty'}</i>}
</div>
))}
<div className={cn('relative flex-1 flex', stl.timeBarWrapper)}>
<BarRow resource={row} timestart={timestart} timewidth={timewidth} popup={renderPopup} />
</div>
</div>
);
};
onPrevClick = () => {
let prevRedIndex = -1;
for (let i = this.state.firstVisibleRowIndex - 1; i >= 0; i--) {
if (this.props.rows[i].isRed()) {
prevRedIndex = i;
break;
}
}
if (this.scroller.current != null) {
this.scroller.current.scrollToRow(prevRedIndex);
}
};
onNextClick = () => {
let prevRedIndex = -1;
for (let i = this.state.firstVisibleRowIndex + 1; i < this.props.rows.length; i++) {
if (this.props.rows[i].isRed()) {
prevRedIndex = i;
break;
}
}
if (this.scroller.current != null) {
this.scroller.current.scrollToRow(prevRedIndex);
}
};
render() {
const { className, rows, children: columns, navigation = false, referenceLines = [], additionalHeight = 0, activeIndex } = this.props;
const { timewidth, timestart } = this.state;
_additionalHeight = additionalHeight;
const sectionDuration = Math.round(timewidth / TIME_SECTIONS_COUNT);
const timeColumns = [];
if (timewidth > 0) {
for (let i = 0; i < TIME_SECTIONS_COUNT; i++) {
timeColumns.push(timestart + i * sectionDuration);
}
}
const visibleRefLines = referenceLines.filter(({ time }) => time > timestart && time < timestart + timewidth);
const columnsSumWidth = columns.reduce((sum, { width }) => sum + width, 0);
return (
<div className={cn(className, 'relative')}>
{navigation && (
<div className={cn(autoscrollStl.navButtons, 'flex items-center')}>
<Button
variant="text-primary"
icon="chevron-up"
tooltip={{
title: 'Previous Error',
delay: 0,
}}
onClick={this.onPrevClick}
/>
<Button
variant="text-primary"
icon="chevron-down"
tooltip={{
title: 'Next Error',
delay: 0,
}}
onClick={this.onNextClick}
/>
{/* <IconButton
size="small"
icon="chevron-up"
return (
<div className={cn(className, 'relative')}>
{navigation && (
<div className={cn(autoscrollStl.navButtons, 'flex items-center')}>
<Button
variant="text-primary"
icon="chevron-up"
tooltip={{
title: 'Previous Error',
delay: 0,
}}
onClick={this.onPrevClick}
/> */}
{/* <IconButton
size="small"
/>
<Button
variant="text-primary"
icon="chevron-down"
tooltip={{
title: 'Next Error',
delay: 0,
}}
onClick={this.onNextClick}
/> */}
</div>
)}
<div className={stl.headers}>
<div className={stl.infoHeaders}>
{columns.map(({ label, width }) => (
<div className={stl.headerCell} style={{ width: `${width}px` }}>
{label}
</div>
))}
</div>
<div className={stl.waterfallHeaders}>
{timeColumns.map((time, i) => (
<div className={stl.timeCell} key={`tc-${i}`}>
{formatTime(time)}
</div>
))}
</div>
</div>
/>
</div>
)}
<div className={stl.headers}>
<div className={stl.infoHeaders}>
{columns.map(({ label, width }) => (
<div className={stl.headerCell} style={{ width: `${width}px` }}>
{label}
</div>
))}
</div>
<div className={stl.waterfallHeaders}>
{timeColumns.map((time, i) => (
<div className={stl.timeCell} key={`tc-${i}`}>
{formatTime(time)}
</div>
))}
</div>
</div>
<NoContent size="small" show={rows.length === 0} title="No recordings found">
<div className="relative">
<div className={stl.timePart} style={{ left: `${columnsSumWidth}px` }}>
{timeColumns.map((_, index) => (
<div key={`tc-${index}`} className={stl.timeCell} />
))}
{visibleRefLines.map(({ time, color, onClick }) => (
<div
className={cn(stl.refLine, `bg-${color}`)}
style={{
left: `${percentOf(time - timestart, timewidth)}%`,
cursor: typeof onClick === 'function' ? 'click' : 'auto',
}}
onClick={onClick}
/>
))}
</div>
<AutoSizer disableHeight>
{({ width }) => (
<List
ref={this.scroller}
className={stl.list}
height={this.tableHeight + additionalHeight}
width={width}
overscanRowCount={20}
rowCount={rows.length}
rowHeight={ROW_HEIGHT}
rowRenderer={this.renderRow}
onScroll={this.onScroll}
scrollToAlignment="start"
forceUpdateProp={timestart | timewidth | activeIndex}
/>
)}
</AutoSizer>
</div>
</NoContent>
<NoContent size="small" show={rows.length === 0}>
<div className="relative">
<div className={stl.timePart} style={{ left: `${columnsSumWidth}px` }}>
{timeColumns.map((_, index) => (
<div key={`tc-${index}`} className={stl.timeCell} />
))}
{visibleRefLines.map(({ time, color, onClick }) => (
<div
className={cn(stl.refLine, `bg-${color}`)}
style={{
left: `${percentOf(time - timestart, timewidth)}%`,
cursor: typeof onClick === 'function' ? 'click' : 'auto',
}}
onClick={onClick}
/>
))}
</div>
);
}
}
<AutoSizer disableHeight>
{({ width }: { width: number }) => (
<List
ref={this.scroller}
className={stl.list}
height={this.tableHeight + additionalHeight}
width={width}
overscanRowCount={20}
rowCount={rows.length}
rowHeight={ROW_HEIGHT}
rowRenderer={this.renderRow}
onScroll={this.onScroll}
scrollToAlignment="start"
forceUpdateProp={timestart | timewidth | (activeIndex || 0)}
/>
)}
</AutoSizer>
</div>
</NoContent>
</div>
);
}
}

View file

@ -100,5 +100,9 @@ $offset: 10px;
}
.activeRow {
background-color: rgba(54, 108, 217, 0.1);
background-color: $teal-light;
}
.inactiveRow {
opacity: 0.5;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,23 +1,33 @@
import React from 'react'
import cn from 'classnames'
import { IconButton } from 'UI'
import React from 'react';
import cn from 'classnames';
import { IconButton } from 'UI';
import stl from './errorItem.module.css';
import { Duration } from 'luxon';
function ErrorItem({ error = {}, onErrorClick, onJump }) {
function ErrorItem({ error = {}, onErrorClick, onJump, inactive, selected }) {
return (
<div className={ cn(stl.wrapper, 'py-3 px-4 flex cursor-pointer') } onClick={onJump}>
<div className="mr-auto">
<div
className={cn(stl.wrapper, 'py-2 px-4 flex cursor-pointer', {
[stl.inactive]: inactive,
[stl.selected]: selected,
})}
onClick={onJump}
>
<div className={'self-start pr-4 color-red'}>
{Duration.fromMillis(error.time).toFormat('mm:ss.SSS')}
</div>
<div className="mr-auto overflow-hidden">
<div className="color-red mb-1 cursor-pointer code-font">
{error.name}
<span className="color-gray-darkest ml-2">{ error.stack0InfoString }</span>
<span className="color-gray-darkest ml-2">{error.stack0InfoString}</span>
</div>
<div className="text-sm color-gray-medium">{error.message}</div>
</div>
<div className="self-end">
<IconButton plain onClick={onErrorClick} label="DETAILS" />
<div className="self-center">
<IconButton red onClick={onErrorClick} label="DETAILS" />
</div>
</div>
)
);
}
export default ErrorItem
export default ErrorItem;

View file

@ -1,3 +1,11 @@
.wrapper {
border-bottom: solid thin $gray-light-shade;
}
.inactive {
opacity: 0.5;
}
.selected {
background-color: $teal-light;
}

View file

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

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

View file

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

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

View file

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

View file

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

View file

@ -29,7 +29,7 @@ function cssUrlsIndex(css: string): Array<[number, number]> {
return idxs.reverse();
}
function unquote(str: string): [string, string] {
str = str.trim();
str = str ? str.trim() : '';
if (str.length <= 2) {
return [str, ""]
}

View file

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

View file

@ -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('|') }`

View file

@ -8,6 +8,7 @@
"upload:minio": "node ./scripts/upload-minio.js",
"deploy:minio": "yarn build:minio && yarn upload:minio",
"lint": "eslint --fix app; exit 0",
"tsc": "tsc --noEmit --w --incremental false",
"gen:constants": "node ./scripts/constants.js",
"gen:icons": "node ./scripts/icons.ts",
"gen:colors": "node ./scripts/colors.js",
@ -81,6 +82,7 @@
"@babel/preset-typescript": "^7.17.12",
"@babel/runtime": "^7.17.9",
"@openreplay/sourcemap-uploader": "^3.0.0",
"@types/luxon": "^3.0.0",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.4",
"@types/react-redux": "^7.1.24",

View file

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