feat(ui): add consistent timestamps to (almost) all items in the player ui

This also tries to make the autoscroll functionality a bit more consistent, where all items are always shown in the list, but items which have not yet occurred will be partially transparent until they happen.

Due to that change, autoscroll behavior which previously always went all the way to the bottom of a list didn't make sense anymore, so now it scrolls to the current item.
This commit is contained in:
Bart Riepe 2022-08-16 15:07:08 +09:00
parent 21fb26b60d
commit af4160eb20
No known key found for this signature in database
GPG key ID: 47ACAEFA260E783B
53 changed files with 2097 additions and 1750 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,4 @@
/* padding-right: 10px; */
/* border: solid thin $gray-light; */
height: 300px;
padding-top: 2px;
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', {
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

@ -18,11 +18,18 @@
border-bottom: solid thin $gray-light-shade;
}
.timestamp {
padding: 7px 0 7px 15px;
}
.activeRow {
background-color: $teal !important;
color: white !important;
background-color: rgba(54, 108, 217, 0.1) !important;
}
.icon {
padding-top: 4px;
}
.inactiveRow {
opacity: 0.5;
}

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

View file

@ -95,10 +95,10 @@ export default class Network extends React.PureComponent {
};
onRowClick = (e, index) => {
pause();
jump(e.time);
this.setState({ currentIndex: index });
this.props.setTimelinePointer(null);
// pause();
// jump(e.time);
// this.setState({ currentIndex: index });
// this.props.setTimelinePointer(null);
};
onTabClick = (activeTab) => this.setState({ activeTab });
@ -108,8 +108,18 @@ export default class Network extends React.PureComponent {
const filterRE = getRE(value, 'i');
const filtered = resources.filter(({ type, name }) => filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab]));
this.setState({ filter: value, filteredList: value ? filtered : resources, currentIndex: 0 });
};
this.setState({ filter: value, filteredList: value ? filtered : resources, currentIndex: 0 });
}
static getDerivedStateFromProps(nextProps, prevState) {
const { filteredList } = prevState;
if (nextProps.timelinePointer) {
const activeItem = filteredList.find((r) => r.time >= nextProps.timelinePointer.time);
return {
currentIndex: activeItem ? filteredList.indexOf(activeItem) : filteredList.length - 1,
};
}
}
render() {
const {
@ -137,7 +147,7 @@ export default class Network extends React.PureComponent {
resourcesSize={resourcesSize}
transferredSize={transferredSize}
onRowClick={this.onRowClick}
currentIndex={listNow.length - 0}
currentIndex={listNow.length - 1}
/>
</React.Fragment>
);

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

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-3 px-4 ', stl.userEvent, this.getLevelClassname(), {
[stl.inactive]: inactive,
[stl.selected]: selected,
})}
>
<div className={'self-start pr-4'}>
{Duration.fromMillis(userEvent.time).toFormat('mm:ss.SSS')}
</div>
<div className={cn('mr-auto', stl.infoWrapper)}>
<div className={stl.title}>
<Icon {...this.getIconProps()} />
{userEvent.name}
</div>
</div>
<div className="self-center">
<IconButton
outline={!userEvent.isRed()}
red={userEvent.isRed()}
onClick={this.onClickDetails}
label="DETAILS"
/>
</div>
</div>
);
}
}

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;
@ -35,4 +32,12 @@
&::-webkit-scrollbar {
height: 1px;
}
}
.inactive {
opacity: 0.5;
}
.selected {
background-color: rgba(54, 108, 217, 0.1);
}

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

View file

@ -101,4 +101,8 @@ $offset: 10px;
.activeRow {
background-color: rgba(54, 108, 217, 0.1);
}
.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

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

View file

@ -1,3 +1,11 @@
.wrapper {
border-bottom: solid thin $gray-light-shade;
}
.inactive {
opacity: 0.5;
}
.selected {
background-color: rgba(54, 108, 217, 0.1);
}

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;
}
function unquote(str: string): [string, string] {
str = str.trim();
str = str ? str.trim() : '';
if (str.length <= 2) {
return [str, ""]
}

View file

@ -35,6 +35,7 @@
.fill-light-blue-bg { fill: $light-blue-bg }
.fill-white { fill: $white }
.fill-borderColor { fill: $borderColor }
.fill-figmaColors { fill: $figmaColors }
/* color */
.color-main { color: $main }
@ -71,6 +72,7 @@
.color-light-blue-bg { color: $light-blue-bg }
.color-white { color: $white }
.color-borderColor { color: $borderColor }
.color-figmaColors { color: $figmaColors }
/* hover color */
.hover-main:hover { color: $main }
@ -107,6 +109,7 @@
.hover-light-blue-bg:hover { color: $light-blue-bg }
.hover-white:hover { color: $white }
.hover-borderColor:hover { color: $borderColor }
.hover-figmaColors:hover { color: $figmaColors }
.border-main { border-color: $main }
.border-gray-light-shade { border-color: $gray-light-shade }
@ -142,3 +145,4 @@
.border-light-blue-bg { border-color: $light-blue-bg }
.border-white { border-color: $white }
.border-borderColor { border-color: $borderColor }
.border-figmaColors { border-color: $figmaColors }

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

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