diff --git a/frontend/app/components/BugFinder/AutoComplete/AutoComplete.js b/frontend/app/components/BugFinder/AutoComplete/AutoComplete.js
index 832f49768..59ed9d9e9 100644
--- a/frontend/app/components/BugFinder/AutoComplete/AutoComplete.js
+++ b/frontend/app/components/BugFinder/AutoComplete/AutoComplete.js
@@ -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});
}
diff --git a/frontend/app/components/Dashboard/Widgets/common/widgetHOC.js b/frontend/app/components/Dashboard/Widgets/common/widgetHOC.js
index 5f91413f1..341d52245 100644
--- a/frontend/app/components/Dashboard/Widgets/common/widgetHOC.js
+++ b/frontend/app/components/Dashboard/Widgets/common/widgetHOC.js
@@ -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 ?
-
-
-
- {comparing &&
}
-
- {
}
-
-
-
-
-
-
- :
-
- )
- }
- }
\ No newline at end of file
+ return wrapped ? (
+
+
+
+ {comparing && (
+
+ )}
+
+ {
+
+ }
+
+
+
+
+
+
+ ) : (
+
+ );
+ }
+ }
+ return WidgetWrapper;
+ };
diff --git a/frontend/app/components/Dashboard/components/FilterSeries/SeriesName/SeriesName.tsx b/frontend/app/components/Dashboard/components/FilterSeries/SeriesName/SeriesName.tsx
index 5d25e9de9..d6a69c73d 100644
--- a/frontend/app/components/Dashboard/components/FilterSeries/SeriesName/SeriesName.tsx
+++ b/frontend/app/components/Dashboard/components/FilterSeries/SeriesName/SeriesName.tsx
@@ -46,7 +46,7 @@ function SeriesName(props: Props) {
onFocus={() => setEditing(true)}
/>
) : (
- {name.trim() === '' ? 'Seriess ' + (seriesIndex + 1) : name }
+ {name && name.trim() === '' ? 'Series ' + (seriesIndex + 1) : name }
)}
setEditing(true)}>
diff --git a/frontend/app/components/Dashboard/components/WidgetName/WidgetName.tsx b/frontend/app/components/Dashboard/components/WidgetName/WidgetName.tsx
index d0d673df2..998aaece8 100644
--- a/frontend/app/components/Dashboard/components/WidgetName/WidgetName.tsx
+++ b/frontend/app/components/Dashboard/components/WidgetName/WidgetName.tsx
@@ -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(() => {
diff --git a/frontend/app/components/Funnels/FunnelSaveModal/FunnelSaveModal.js b/frontend/app/components/Funnels/FunnelSaveModal/FunnelSaveModal.js
index ca35d401d..e0d43d1d7 100644
--- a/frontend/app/components/Funnels/FunnelSaveModal/FunnelSaveModal.js
+++ b/frontend/app/components/Funnels/FunnelSaveModal/FunnelSaveModal.js
@@ -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 (
-
-
- { 'Save Funnel' }
-
+
+ {'Save Funnel'}
+
@@ -64,11 +65,11 @@ export default class FunnelSaveModal extends React.PureComponent {
@@ -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"
/>
- this.props.edit({ 'isPublic' : !funnel.isPublic }) }>
+
this.props.edit({ isPublic: !funnel.isPublic })}
+ >
Team Visible
@@ -91,16 +95,16 @@ export default class FunnelSaveModal extends React.PureComponent {
-
+
-
+
);
diff --git a/frontend/app/components/Session/IOSPlayer/Network.js b/frontend/app/components/Session/IOSPlayer/Network.js
index ab42a61fa..3956b7031 100644
--- a/frontend/app/components/Session/IOSPlayer/Network.js
+++ b/frontend/app/components/Session/IOSPlayer/Network.js
@@ -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) =>
- { r.url } }
- size="mini"
- position="right center"
- >
- { r.url }
-
- },
- {
- 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) => (
+ {r.url}}
+ size="mini"
+ position="right center"
+ >
+ {r.url}
+
+ ),
+ },
+ {
+ 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 (
- <>
-
+
+ isDisplayed={current != null}
+ content={
+ current && (
+
+ )
}
- onClose={ closeModal }
+ onClose={closeModal}
/>
-
- { COLUMNS }
-
- >
- );
+
+ {COLUMNS}
+
+ >
+ );
}
-export default observer(Network);
\ No newline at end of file
+export default observer(Network);
diff --git a/frontend/app/components/Session/IOSPlayer/StackEvents.js b/frontend/app/components/Session/IOSPlayer/StackEvents.js
index 92470b358..f1abef414 100644
--- a/frontend/app/components/Session/IOSPlayer/StackEvents.js
+++ b/frontend/app/components/Session/IOSPlayer/StackEvents.js
@@ -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 }) =>
-
-);
\ No newline at end of file
+export default observer(({ player }) => );
diff --git a/frontend/app/components/Session_/Autoscroll.js b/frontend/app/components/Session_/Autoscroll.js
deleted file mode 100644
index 02af15417..000000000
--- a/frontend/app/components/Session_/Autoscroll.js
+++ /dev/null
@@ -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 (
-
-
(this.scrollableElement = ref)}>
- {children}
-
- {navigation && (
-
-
-
-
- )}
-
- );
- }
-}
diff --git a/frontend/app/components/Session_/Autoscroll.tsx b/frontend/app/components/Session_/Autoscroll.tsx
new file mode 100644
index 000000000..305b12dad
--- /dev/null
+++ b/frontend/app/components/Session_/Autoscroll.tsx
@@ -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 {
+ state = {
+ autoScroll: true,
+ currentIndex: 0,
+ };
+ scrollableElement = React.createRef()
+
+ 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 (
+
+
+ {children}
+
+
+
+
+ {navigation && (
+ <>
+
+
+ >
+ )}
+
+
+
+ );
+ }
+}
diff --git a/frontend/app/components/Session_/BottomBlock/Header.js b/frontend/app/components/Session_/BottomBlock/Header.js
index 976456332..15dd7a0c9 100644
--- a/frontend/app/components/Session_/BottomBlock/Header.js
+++ b/frontend/app/components/Session_/BottomBlock/Header.js
@@ -13,7 +13,7 @@ const Header = ({
showClose = true,
...props
}) => (
-
+
{ children }
{ showClose &&
}
diff --git a/frontend/app/components/Session_/BottomBlock/InfoLine.js b/frontend/app/components/Session_/BottomBlock/InfoLine.js
index 8872be906..d4607a887 100644
--- a/frontend/app/components/Session_/BottomBlock/InfoLine.js
+++ b/frontend/app/components/Session_/BottomBlock/InfoLine.js
@@ -11,7 +11,7 @@ const InfoLine = ({ children }) => (
const Point = ({ label, value, display=true, color, dotColor }) => display
?
{ dotColor != null &&
}
-
{ `${label}:` } { value }
+
{ `${label}` } { value }
: null;
diff --git a/frontend/app/components/Session_/BottomBlock/bottomBlock.module.css b/frontend/app/components/Session_/BottomBlock/bottomBlock.module.css
index 41cf7e5e1..cda4ec372 100644
--- a/frontend/app/components/Session_/BottomBlock/bottomBlock.module.css
+++ b/frontend/app/components/Session_/BottomBlock/bottomBlock.module.css
@@ -4,6 +4,4 @@
/* padding-right: 10px; */
/* border: solid thin $gray-light; */
height: 300px;
- padding-top: 2px;
- border-top: thin dashed #cccccc
}
diff --git a/frontend/app/components/Session_/BottomBlock/infoLine.module.css b/frontend/app/components/Session_/BottomBlock/infoLine.module.css
index d03a53439..b6798d1bf 100644
--- a/frontend/app/components/Session_/BottomBlock/infoLine.module.css
+++ b/frontend/app/components/Session_/BottomBlock/infoLine.module.css
@@ -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;
}
}
}
diff --git a/frontend/app/components/Session_/Console/Console.js b/frontend/app/components/Session_/Console/Console.js
index 5534439fb..3c4a3752c 100644
--- a/frontend/app/components/Session_/Console/Console.js
+++ b/frontend/app/components/Session_/Console/Console.js
@@ -12,7 +12,7 @@ export default class Console extends React.PureComponent {
render() {
const { logs, time, listNow } = this.props;
return (
-
+
);
}
}
diff --git a/frontend/app/components/Session_/Console/ConsoleContent.js b/frontend/app/components/Session_/Console/ConsoleContent.js
index 29820de2e..012582849 100644
--- a/frontend/app/components/Session_/Console/ConsoleContent.js
+++ b/frontend/app/components/Session_/Console/ConsoleContent.js
@@ -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) =>
{line}
);
+ if (typeof s !== 'string') return '';
+ return s.split('\n').map((line, i) =>
{line}
);
}
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 (
- <>
-
-
-
- Console
-
-
-
-
-
-
-
- No {activeTab === ALL ? 'Data' : activeTab.toLowerCase()} }
- size="small"
- show={filtered.length === 0}
- >
-
- {filtered.map((l, index) => (
- !isResult && jump(l.time)}
- >
-
-
{renderWithNL(l.value)}
-
- ))}
-
-
-
-
- >
- );
- }
+ const lastIndex = filtered.filter((item) => item.time <= time).length - 1;
+
+ return (
+ <>
+
+
+
+ Console
+
+
+
+
+
+
+
+ {filtered.map((l, index) => (
+ lastIndex,
+ 'cursor-pointer': !isResult,
+ })}
+ onClick={() => !isResult && jump(l.time)}
+ >
+
+
+
+
+ {Duration.fromMillis(l.time).toFormat('mm:ss.SSS')}
+
+
+
{renderWithNL(l.value)}
+
+
+ ))}
+
+
+
+
+ >
+ );
+ }
}
diff --git a/frontend/app/components/Session_/Console/console.module.css b/frontend/app/components/Session_/Console/console.module.css
index 55d19c7bd..66ffc32f9 100644
--- a/frontend/app/components/Session_/Console/console.module.css
+++ b/frontend/app/components/Session_/Console/console.module.css
@@ -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;
}
\ No newline at end of file
diff --git a/frontend/app/components/Session_/Exceptions/Exceptions.js b/frontend/app/components/Session_/Exceptions/Exceptions.js
index 5c5e913e7..0a30a025c 100644
--- a/frontend/app/components/Session_/Exceptions/Exceptions.js
+++ b/frontend/app/components/Session_/Exceptions/Exceptions.js
@@ -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 (
<>
-
-
-
-
{currentError.name}
-
-
- {currentError.function}
-
+
+
+
+ {currentError.name}
+
+ {currentError.function}
+
+ {currentError.message}
- {currentError.message}
-
+ )
}
- isDisplayed={ currentError != null }
- content={ currentError &&
-
-
-
-
-
-
-
+ isDisplayed={currentError != null}
+ content={
+ currentError && (
+
+
+
+
+
+
+
+ )
}
- onClose={ this.closeModal }
+ onClose={this.closeModal}
/>
-
+ Exceptions
+
+
+
-
-
- { filtered.map(e => (
- jump(e.time) }
- error={e}
- key={e.key}
- onErrorClick={() => this.setCurrentError(e)}
- />
- ))
- }
+
+
+ {filtered.map((e, index) => (
+ jump(e.time)}
+ error={e}
+ key={e.key}
+ selected={lastIndex === index}
+ inactive={index > lastIndex}
+ onErrorClick={(jsEvent) => {
+ jsEvent.stopPropagation();
+ jsEvent.preventDefault();
+ this.setCurrentError(e);
+ }}
+ />
+ ))}
diff --git a/frontend/app/components/Session_/Fetch/Fetch.js b/frontend/app/components/Session_/Fetch/Fetch.js
index ab847f3fe..d8078e89a 100644
--- a/frontend/app/components/Session_/Fetch/Fetch.js
+++ b/frontend/app/components/Session_/Fetch/Fetch.js
@@ -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 (
-
-
- Fetch Request
-
-
-
Status
-
-
-
-
-
- }
- isDisplayed={current != null && showFetchDetails}
- content={
- current &&
- showFetchDetails && (
-
- )
- }
- onClose={this.closeModal}
- />
-
-
- Fetch
-
-
-
-
-
-
-
- No Data
-
- }
- // size="small"
- show={filteredList.length === 0}
- >
- {/* */}
-
- {[
- {
- label: 'Status',
- dataKey: 'status',
- width: 70,
- },
- {
- label: 'Method',
- dataKey: 'method',
- width: 60,
- },
- {
- label: 'Name',
- width: 240,
- render: renderName,
- },
- {
- label: 'Time',
- width: 80,
- render: renderDuration,
- },
- ]}
-
-
-
-
-
- );
+ 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 (
+
+
+ Fetch Request
+
+
+
Status
+
+
+
+
+
+ }
+ isDisplayed={current != null && showFetchDetails}
+ content={
+ current &&
+ showFetchDetails && (
+
+ )
+ }
+ onClose={this.closeModal}
+ />
+
+
+ Fetch
+
+
+
+
+
+
+
+ No Data
+
+ } show={filteredList.length === 0}>
+
+ {[
+ {
+ 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,
+ },
+ ]}
+
+
+
+
+
+ );
+ }
}
diff --git a/frontend/app/components/Session_/GraphQL/GQLDetails.js b/frontend/app/components/Session_/GraphQL/GQLDetails.js
index 47ec43239..4caba50a7 100644
--- a/frontend/app/components/Session_/GraphQL/GQLDetails.js
+++ b/frontend/app/components/Session_/GraphQL/GQLDetails.js
@@ -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 (
-
-
{ 'Operation Name'}
-
{ operationName }
+ 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 (
+
+
{'Operation Name'}
+
{operationName}
-
-
-
Operation Kind
-
{operationKind}
-
-
-
Duration
-
{parseInt(duration)} ms
-
-
+
+
+
Operation Kind
+
{operationKind}
+
+
+
Duration
+
{duration ? parseInt(duration) : '???'} ms
+
+
-
-
{ 'Response' }
-
-
- { variables && variables !== "{}" &&
-
-
-
{ 'Variables'}
- { jsonVars === undefined
- ?
{ variables }
- :
- }
-
-
-
- }
-
- { jsonResponse === undefined
- ?
{ response }
- :
- }
-
-
+
+
+
+
{'Variables'}
+
+
+ {jsonVars === undefined ? variables : }
+
+
+
-
-
-
-
-
- );
- }
-}
\ No newline at end of file
+
+
+
{'Response'}
+
+
+ {jsonResponse === undefined ? response : }
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/frontend/app/components/Session_/GraphQL/GraphQL.js b/frontend/app/components/Session_/GraphQL/GraphQL.js
index 4b29a497b..4c613454b 100644
--- a/frontend/app/components/Session_/GraphQL/GraphQL.js
+++ b/frontend/app/components/Session_/GraphQL/GraphQL.js
@@ -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 (
+
+
{r.operationName}
+
+
+ );
+}
+
+@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 (
-
GraphQL
-
+
}
- isDisplayed={ current != null }
- content={ current &&
-
+ isDisplayed={current != null}
+ content={
+ current && (
+
+ )
}
- onClose={ this.closeModal }
+ onClose={this.closeModal}
/>
@@ -93,36 +142,38 @@ export default class GraphQL extends React.PureComponent {
icon="search"
iconPosition="left"
name="filter"
- onChange={ this.onFilterChange }
+ onChange={this.onFilterChange}
/>
-
+
{[
{
- 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,
},
]}
diff --git a/frontend/app/components/Session_/Network/Network.js b/frontend/app/components/Session_/Network/Network.js
index 887cc1148..096a38a2a 100644
--- a/frontend/app/components/Session_/Network/Network.js
+++ b/frontend/app/components/Session_/Network/Network.js
@@ -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}
/>
);
diff --git a/frontend/app/components/Session_/Network/NetworkContent.js b/frontend/app/components/Session_/Network/NetworkContent.js
index b6c54b5b4..b3243c071 100644
--- a/frontend/app/components/Session_/Network/NetworkContent.js
+++ b/frontend/app/components/Session_/Network/NetworkContent.js
@@ -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 (
- {r.type}}>
- {r.type}
-
- );
+ return (
+ {r.type}}>
+ {r.type}
+
+ );
}
export function renderName(r) {
- return (
- {r.url}}>
- {r.name}
-
- );
+ return (
+ }
+ >
+ {r.name}
+
+
+
+ );
+}
+
+export function renderStart(r) {
+ return Duration.fromMillis(r.time).toFormat('mm:ss.SSS');
}
const renderXHRText = () => (
-
- {XHR}
-
- Use our{' '}
-
- Fetch plugin
-
- {' to capture HTTP requests and responses, including status codes and bodies.'}
- We also provide{' '}
-
- support for GraphQL
-
- {' for easy debugging of your queries.'}
- >
- }
- className="ml-1"
- />
-
+
+ {XHR}
+
+ Use our{' '}
+
+ Fetch plugin
+
+ {' to capture HTTP requests and responses, including status codes and bodies.'}
+ We also provide{' '}
+
+ support for GraphQL
+
+ {' for easy debugging of your queries.'}
+ >
+ }
+ className="ml-1"
+ />
+
);
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 = (
-
- {showTransferred && - {`${formatBytes(r.encodedBodySize + headerSize)} transfered over network`}
}
- - {`Resource size: ${formatBytes(r.decodedBodySize)} `}
-
- );
- }
-
- return (
-
- {triggerText}
-
+ triggerText = formatBytes(r.decodedBodySize);
+ content = (
+
+ {showTransferred && (
+ - {`${formatBytes(r.encodedBodySize + headerSize)} transfered over network`}
+ )}
+ - {`Resource size: ${formatBytes(r.decodedBodySize)} `}
+
);
+ }
+
+ return (
+
+ {triggerText}
+
+ );
}
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 (
-
- {text}
-
- );
+ return (
+
+ {text}
+
+ );
}
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 (
-
-
-
-
- Network
-
-
-
-
-
-
-
- 0} />
- 0} />
-
-
-
-
-
-
- No Data
-
- }
- size="small"
- show={filtered.length === 0}
- >
-
- {[
- {
- 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,
- },
- ]}
-
-
-
-
-
- );
+ 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 (
+
+
+
+
+
+
+
+ {/* */}
+ {/*
*/}
+ {/*
{ location }
*/}
+ {/*
*/}
+ {/*
*/}
+
+
+ 0}
+ />
+ 0}
+ />
+
+
+
+
+
+
+ No Data
+
+ }
+ size="small"
+ show={filtered.length === 0}
+ >
+
+ {[
+ {
+ 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,
+ },
+ ]}
+
+
+
+
+
+ );
+ }
}
diff --git a/frontend/app/components/Session_/Player/Overlay/ElementsMarker/Marker.tsx b/frontend/app/components/Session_/Player/Overlay/ElementsMarker/Marker.tsx
index d5afa65a2..cc8f3fd1f 100644
--- a/frontend/app/components/Session_/Player/Overlay/ElementsMarker/Marker.tsx
+++ b/frontend/app/components/Session_/Player/Overlay/ElementsMarker/Marker.tsx
@@ -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 (
- activeTarget(target.index)}>
-
{target.index + 1}
-
{target.count} Clicks
- )}
- >
-
-
-
- )
+ activeTarget(target.index)}>
+
{target.index + 1}
+ {/* @ts-expect-error Tooltip doesn't have children property */}
+
{target.count} Clicks
+ )}
+ trigger="mouseenter"
+ >
+
+
+
+ )
}
\ No newline at end of file
diff --git a/frontend/app/components/Session_/StackEvents/StackEvents.js b/frontend/app/components/Session_/StackEvents/StackEvents.js
index 8069cb663..f4a9387d6 100644
--- a/frontend/app/components/Session_/StackEvents/StackEvents.js
+++ b/frontend/app/components/Session_/StackEvents/StackEvents.js
@@ -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 ;
+ case DATADOG:
+ return ;
+ case STACKDRIVER:
+ return ;
+ default:
+ return ;
+ }
+ }
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 (
-
-
-
- Events
-
-
-
-
-
- Integrations
- {' and '}
- Events
- { ' make debugging easier. Sync your backend logs and custom events with session replay.' }
-
-
- >
- : null
- }
- size="small"
- show={ filteredStackEvents.length === 0 }
- >
-
- { filteredStackEvents.map(userEvent => (
- jump(userEvent.time) }
- />
- ))}
-
-
-
-
+ <>
+
+
+
+ {currentEvent.name}
+
+ {currentEvent.function}
+
+ {currentEvent.message}
+
+ )
+ }
+ isDisplayed={currentEvent != null}
+ content={
+ currentEvent && {this.renderPopupContent(currentEvent)}
+ }
+ onClose={this.closeModal.bind(this)}
+ />
+
+
+
+ Events
+
+
+
+
+
+
+ Integrations
+
+ {' and '}
+
+ Events
+
+ {
+ ' make debugging easier. Sync your backend logs and custom events with session replay.'
+ }
+
+
+
+ >
+ ) : null
+ }
+ size="small"
+ show={filteredStackEvents.length === 0}
+ >
+
+ {filteredStackEvents.map((userEvent, index) => (
+ lastIndex}
+ selected={lastIndex === index}
+ userEvent={userEvent}
+ onJump={() => jump(userEvent.time)}
+ />
+ ))}
+
+
+
+
+ >
);
}
}
diff --git a/frontend/app/components/Session_/StackEvents/UserEvent/UserEvent.js b/frontend/app/components/Session_/StackEvents/UserEvent/UserEvent.js
index a40da51f8..1c657a2d8 100644
--- a/frontend/app/components/Session_/StackEvents/UserEvent/UserEvent.js
+++ b/frontend/app/components/Session_/StackEvents/UserEvent/UserEvent.js
@@ -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 ;
- case DATADOG:
- return ;
- case STACKDRIVER:
- return ;
- default:
- return ;
- }
- }
-
- ifNeedModal() {
- return !!this.props.userEvent.payload;
- }
-
- onClickDetails = (e) => {
- e.stopPropagation();
- this.props.switchOpen();
- }
-
- renderContent(modalTrigger) {
- const { userEvent } = this.props;
- //const message = this.getEventMessage();
- return (
-
-
-
-
- { userEvent.name }
-
- { /* message &&
-
- { message }
-
*/
- }
-
-
-
-
-
- );
- }
-
- render() {
- const { userEvent } = this.props;
- if (this.ifNeedModal()) {
- return (
-
-
- { this.renderContent(true) }
-
- //
- );
- }
- return this.renderContent();
- }
+ render() {
+ const { userEvent, inactive, selected } = this.props;
+ //const message = this.getEventMessage();
+ return (
+
+
+ {Duration.fromMillis(userEvent.time).toFormat('mm:ss.SSS')}
+
+
+
+
+ {userEvent.name}
+
+
+
+
+
+
+ );
+ }
}
diff --git a/frontend/app/components/Session_/StackEvents/UserEvent/userEvent.module.css b/frontend/app/components/Session_/StackEvents/UserEvent/userEvent.module.css
index 57388ffe5..ce5d27afa 100644
--- a/frontend/app/components/Session_/StackEvents/UserEvent/userEvent.module.css
+++ b/frontend/app/components/Session_/StackEvents/UserEvent/userEvent.module.css
@@ -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);
}
\ No newline at end of file
diff --git a/frontend/app/components/Session_/TimeTable/BarRow.js b/frontend/app/components/Session_/TimeTable/BarRow.js
deleted file mode 100644
index b53661403..000000000
--- a/frontend/app/components/Session_/TimeTable/BarRow.js
+++ /dev/null
@@ -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 = (
-
- );
- if (!popup) return { trigger }
;
-
- return (
-
-
- { ttfb != null &&
-
-
{ 'Waiting (TTFB)' }
-
-
{ formatTime(ttfb) }
-
- }
-
-
{ 'Content Download' }
-
-
{ formatTime(duration - ttfb) }
-
-
- }
- >
- {trigger}
-
-
- );
-}
-
-BarRow.displayName = "BarRow";
-
-export default BarRow;
diff --git a/frontend/app/components/Session_/TimeTable/BarRow.tsx b/frontend/app/components/Session_/TimeTable/BarRow.tsx
new file mode 100644
index 000000000..9de1a8279
--- /dev/null
+++ b/frontend/app/components/Session_/TimeTable/BarRow.tsx
@@ -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 = (
+
+ );
+ if (!popup) return {trigger}
;
+
+ return (
+
+
+ {ttfb != null &&
+
+
{'Waiting (TTFB)'}
+
+
{formatTime(ttfb)}
+
+ }
+
+
{'Content Download'}
+
+
{formatTime(duration - ttfb)}
+
+
+ }
+ size="mini"
+ position="top center"
+ />
+
+ );
+}
+
+BarRow.displayName = "BarRow";
+
+export default BarRow;
\ No newline at end of file
diff --git a/frontend/app/components/Session_/TimeTable/TimeTable.tsx b/frontend/app/components/Session_/TimeTable/TimeTable.tsx
index 04cdf39d5..886e3074a 100644
--- a/frontend/app/components/Session_/TimeTable/TimeTable.tsx
+++ b/frontend/app/components/Session_/TimeTable/TimeTable.tsx
@@ -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;
- style?: Object;
+ label: string;
+ width: number;
+ dataKey?: string;
+ render?: (row: any) => void
+ referenceLines?: Array;
+ 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;
- children: Array;
+ className?: string;
+ rows: Array;
+ children: Array;
+ 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, 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, 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 {
- 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();
+ 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 (
+ activeIndex,
+ })}
+ onClick={typeof onRowClick === 'function' ? () => onRowClick(row, index) : undefined}
+ id="table-row"
+ >
+ {columns.map(({ dataKey, render, width }) => (
+
+ {render ? render(row) : row[dataKey || ''] || {'empty'}}
+
+ ))}
+
+
+
+
+ );
+ };
+
+ 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 (
- onRowClick(row, index) : null}
- id="table-row"
- >
- {columns.map(({ dataKey, render, width }) => (
-
- {render ? render(row) : row[dataKey] || {'empty'}}
-
- ))}
-
-
-
-
- );
- };
-
- 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 (
-
- {navigation && (
-
-
-
- {/*
+ {navigation && (
+
+ */}
- {/*
+ */}
-
- )}
-
-
- {columns.map(({ label, width }) => (
-
- {label}
-
- ))}
-
-
- {timeColumns.map((time, i) => (
-
- {formatTime(time)}
-
- ))}
-
-
+ />
+
+ )}
+
+
+ {columns.map(({ label, width }) => (
+
+ {label}
+
+ ))}
+
+
+ {timeColumns.map((time, i) => (
+
+ {formatTime(time)}
+
+ ))}
+
+
-
-
-
- {timeColumns.map((_, index) => (
-
- ))}
- {visibleRefLines.map(({ time, color, onClick }) => (
-
- ))}
-
-
- {({ width }) => (
-
- )}
-
-
-
+
+
+
+ {timeColumns.map((_, index) => (
+
+ ))}
+ {visibleRefLines.map(({ time, color, onClick }) => (
+
+ ))}
- );
- }
-}
+
+ {({ width }: { width: number }) => (
+
+ )}
+
+
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/frontend/app/components/Session_/TimeTable/timeTable.module.css b/frontend/app/components/Session_/TimeTable/timeTable.module.css
index 643f02012..6a22a9d57 100644
--- a/frontend/app/components/Session_/TimeTable/timeTable.module.css
+++ b/frontend/app/components/Session_/TimeTable/timeTable.module.css
@@ -101,4 +101,8 @@ $offset: 10px;
.activeRow {
background-color: rgba(54, 108, 217, 0.1);
+}
+
+.inactiveRow {
+ opacity: 0.5;
}
\ No newline at end of file
diff --git a/frontend/app/components/Session_/autoscroll.module.css b/frontend/app/components/Session_/autoscroll.module.css
index 42c5d980a..209badfb2 100644
--- a/frontend/app/components/Session_/autoscroll.module.css
+++ b/frontend/app/components/Session_/autoscroll.module.css
@@ -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;
}
diff --git a/frontend/app/components/hocs/withLocationHandlers.js b/frontend/app/components/hocs/withLocationHandlers.js
index a202f178e..b386690b8 100644
--- a/frontend/app/components/hocs/withLocationHandlers.js
+++ b/frontend/app/components/hocs/withLocationHandlers.js
@@ -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 (
-
- );
+ return ;
}
- };
-
+ }
+ return WrapperClass;
+};
export default withLocationHandlers;
diff --git a/frontend/app/components/hocs/withPermissions.js b/frontend/app/components/hocs/withPermissions.js
index 1f4e6ade8..f31730553 100644
--- a/frontend/app/components/hocs/withPermissions.js
+++ b/frontend/app/components/hocs/withPermissions.js
@@ -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 ? (
-
- ) : (
-
- {isReplay ? (
-
- ) : (
-
- )}
-
- );
- }
- }
- );
+ return !this.props.isEnterprise || hasPermission ? (
+
+ ) : (
+
+ {isReplay ? (
+
+ ) : (
+
+ )}
+
+ );
+ }
+ }
+ return WrapperClass
+}
\ No newline at end of file
diff --git a/frontend/app/components/hocs/withSiteIdRouter.js b/frontend/app/components/hocs/withSiteIdRouter.js
index 4dbaf623c..ee41610ce 100644
--- a/frontend/app/components/hocs/withSiteIdRouter.js
+++ b/frontend/app/components/hocs/withSiteIdRouter.js
@@ -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
}
}
-
- render() {
- const { history, ...other } = this.props
-
- return
- }
-}
\ No newline at end of file
+ return WrappedClass
+}
\ No newline at end of file
diff --git a/frontend/app/components/hocs/withSiteIdUpdater.js b/frontend/app/components/hocs/withSiteIdUpdater.js
index 3abb48c09..1c4e038ae 100644
--- a/frontend/app/components/hocs/withSiteIdUpdater.js
+++ b/frontend/app/components/hocs/withSiteIdUpdater.js
@@ -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 : ;
}
}
- render() {
- return this.state.load ? null : ;
- }
-}
\ No newline at end of file
+ return WrapperClass
+}
\ No newline at end of file
diff --git a/frontend/app/components/shared/CustomMetrics/FilterSeries/SeriesName/SeriesName.tsx b/frontend/app/components/shared/CustomMetrics/FilterSeries/SeriesName/SeriesName.tsx
index 5d25e9de9..d6a69c73d 100644
--- a/frontend/app/components/shared/CustomMetrics/FilterSeries/SeriesName/SeriesName.tsx
+++ b/frontend/app/components/shared/CustomMetrics/FilterSeries/SeriesName/SeriesName.tsx
@@ -46,7 +46,7 @@ function SeriesName(props: Props) {
onFocus={() => setEditing(true)}
/>
) : (
- {name.trim() === '' ? 'Seriess ' + (seriesIndex + 1) : name }
+ {name && name.trim() === '' ? 'Series ' + (seriesIndex + 1) : name }
)}
setEditing(true)}>
diff --git a/frontend/app/components/ui/ErrorItem/ErrorItem.js b/frontend/app/components/ui/ErrorItem/ErrorItem.js
index f1145ac71..3eab87fe2 100644
--- a/frontend/app/components/ui/ErrorItem/ErrorItem.js
+++ b/frontend/app/components/ui/ErrorItem/ErrorItem.js
@@ -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 (
-
-
+
+
{Duration.fromMillis(error.time).toFormat('mm:ss.SSS')}
+
{error.name}
{ error.stack0InfoString }
{error.message}
-
)
diff --git a/frontend/app/components/ui/ErrorItem/errorItem.module.css b/frontend/app/components/ui/ErrorItem/errorItem.module.css
index 5a185ed5c..45b2a12c1 100644
--- a/frontend/app/components/ui/ErrorItem/errorItem.module.css
+++ b/frontend/app/components/ui/ErrorItem/errorItem.module.css
@@ -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);
}
\ No newline at end of file
diff --git a/frontend/app/components/ui/LinkStyledInput/LinkStyledInput.js b/frontend/app/components/ui/LinkStyledInput/LinkStyledInput.js
index eb579507e..110d0e6d7 100644
--- a/frontend/app/components/ui/LinkStyledInput/LinkStyledInput.js
+++ b/frontend/app/components/ui/LinkStyledInput/LinkStyledInput.js
@@ -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,
});
}
diff --git a/frontend/app/components/ui/NoContent/NoContent.js b/frontend/app/components/ui/NoContent/NoContent.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/app/components/ui/Pagination/Pagination.tsx b/frontend/app/components/ui/Pagination/Pagination.tsx
index b36d2b397..6d131fa96 100644
--- a/frontend/app/components/ui/Pagination/Pagination.tsx
+++ b/frontend/app/components/ui/Pagination/Pagination.tsx
@@ -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 (
{
- 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 (
-
-
- { trigger }
-
-
- );
- }
-}
\ No newline at end of file
diff --git a/frontend/app/components/ui/Tooltip/Tooltip.tsx b/frontend/app/components/ui/Tooltip/Tooltip.tsx
new file mode 100644
index 000000000..6a14ce3f7
--- /dev/null
+++ b/frontend/app/components/ui/Tooltip/Tooltip.tsx
@@ -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 {
+ 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 (
+
+
+ { trigger }
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/frontend/app/components/ui/Tooltip/index.js b/frontend/app/components/ui/Tooltip/index.ts
similarity index 100%
rename from frontend/app/components/ui/Tooltip/index.js
rename to frontend/app/components/ui/Tooltip/index.ts
diff --git a/frontend/app/declaration.d.ts b/frontend/app/declaration.d.ts
new file mode 100644
index 000000000..0463954c3
--- /dev/null
+++ b/frontend/app/declaration.d.ts
@@ -0,0 +1,9 @@
+declare module '*.scss' {
+ const content: Record;
+ export default content;
+}
+
+declare module '*.css' {
+ const content: Record;
+ export default content;
+}
\ No newline at end of file
diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Marker.js b/frontend/app/player/MessageDistributor/StatedScreen/Screen/Marker.js
index 5461422cf..daf5a67b4 100644
--- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Marker.js
+++ b/frontend/app/player/MessageDistributor/StatedScreen/Screen/Marker.js
@@ -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 = "Right-click \> Inspect 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 = 'Right-click > Inspect 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 = `${tag.tagName.toLowerCase()}`
+ const attrs = tag.attributes;
+ let str = `${tag.tagName.toLowerCase()}`;
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 += `${'.' + k.value.split(' ').join('.')}`
+ str += `${'.' + k.value.split(' ').join('.')}`;
}
if (attribute === 'id') {
- str += `${'#' + k.value.split(' ').join('#')}`
+ str += `${'#' + k.value.split(' ').join('#')}`;
}
}
@@ -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);
}
-
-}
\ No newline at end of file
+}
diff --git a/frontend/app/player/MessageDistributor/managers/DOM/DOMManager.ts b/frontend/app/player/MessageDistributor/managers/DOM/DOMManager.ts
index 378021203..db850842f 100644
--- a/frontend/app/player/MessageDistributor/managers/DOM/DOMManager.ts
+++ b/frontend/app/player/MessageDistributor/managers/DOM/DOMManager.ts
@@ -103,6 +103,7 @@ export default class DOMManager extends ListWalker {
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);
diff --git a/frontend/app/player/MessageDistributor/messages/urlResolve.ts b/frontend/app/player/MessageDistributor/messages/urlResolve.ts
index 44298ec08..07a7381fb 100644
--- a/frontend/app/player/MessageDistributor/messages/urlResolve.ts
+++ b/frontend/app/player/MessageDistributor/messages/urlResolve.ts
@@ -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, ""]
}
diff --git a/frontend/app/styles/colors-autogen.css b/frontend/app/styles/colors-autogen.css
index d1fd5a0a9..42ae94dab 100644
--- a/frontend/app/styles/colors-autogen.css
+++ b/frontend/app/styles/colors-autogen.css
@@ -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 }
diff --git a/frontend/app/utils.ts b/frontend/app/utils.ts
index 03202fff0..52bf7c6ad 100644
--- a/frontend/app/utils.ts
+++ b/frontend/app/utils.ts
@@ -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) {
diff --git a/frontend/app/validate.js b/frontend/app/validate.js
index 687091003..76d588ac9 100644
--- a/frontend/app/validate.js
+++ b/frontend/app/validate.js
@@ -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('|') }`
diff --git a/frontend/package.json b/frontend/package.json
index 957c65c3a..3bc21e1f9 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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",
diff --git a/frontend/webpack.config.ts b/frontend/webpack.config.ts
index 4972b3213..404531b55 100644
--- a/frontend/webpack.config.ts
+++ b/frontend/webpack.config.ts
@@ -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: [