diff --git a/frontend/app/components/BugFinder/SessionList/SessionList.js b/frontend/app/components/BugFinder/SessionList/SessionList.js index 858e9cb30..27324e686 100644 --- a/frontend/app/components/BugFinder/SessionList/SessionList.js +++ b/frontend/app/components/BugFinder/SessionList/SessionList.js @@ -1,12 +1,12 @@ import { connect } from 'react-redux'; -import { Loader, NoContent, Button, LoadMoreButton, Pagination } from 'UI'; +import { Loader, NoContent, Button, Pagination } from 'UI'; import { applyFilter, addAttribute, addEvent } from 'Duck/filters'; -import { fetchSessions, addFilterByKeyAndValue, updateCurrentPage } from 'Duck/search'; +import { fetchSessions, addFilterByKeyAndValue, updateCurrentPage, setScrollPosition } from 'Duck/search'; import SessionItem from 'Shared/SessionItem'; import SessionListHeader from './SessionListHeader'; import { FilterKey } from 'Types/filter/filterType'; -const ALL = 'all'; +// const ALL = 'all'; const PER_PAGE = 10; const AUTOREFRESH_INTERVAL = 3 * 60 * 1000; var timeoutId; @@ -21,6 +21,7 @@ var timeoutId; filters: state.getIn([ 'search', 'instance', 'filters' ]), metaList: state.getIn(['customFields', 'list']).map(i => i.key), currentPage: state.getIn([ 'search', 'currentPage' ]), + scrollY: state.getIn([ 'search', 'scrollY' ]), }), { applyFilter, addAttribute, @@ -28,24 +29,15 @@ var timeoutId; fetchSessions, addFilterByKeyAndValue, updateCurrentPage, + setScrollPosition, }) export default class SessionList extends React.PureComponent { - state = { - showPages: 1, - } + constructor(props) { super(props); this.timeout(); } - componentDidUpdate(prevProps) { - if (prevProps.loading && !this.props.loading) { - this.setState({ showPages: 1 }); - } - } - - addPage = () => this.setState({ showPages: this.state.showPages + 1 }) - onUserClick = (userId, userAnonymousId) => { if (userId) { this.props.addFilterByKeyAndValue(FilterKey.USERID, userId); @@ -75,17 +67,22 @@ export default class SessionList extends React.PureComponent { } componentWillUnmount() { + this.props.setScrollPosition(window.scrollY) clearTimeout(timeoutId) } - + componentDidMount() { + const { scrollY } = this.props; + console.log('scrollY', scrollY); + window.scrollTo(0, scrollY); + } renderActiveTabContent(list) { const { loading, filters, - onMenuItemClick, - allList, + // onMenuItemClick, + // allList, activeTab, metaList, currentPage, @@ -93,8 +90,6 @@ export default class SessionList extends React.PureComponent { } = this.props; const _filterKeys = filters.map(i => i.key); const hasUserFilter = _filterKeys.includes(FilterKey.USERID) || _filterKeys.includes(FilterKey.USERANONYMOUSID); - const { showPages } = this.state; - const displayedCount = Math.min(showPages * PER_PAGE, list.size); return (
Please try changing your search parameters.
- {allList.size > 0 && ( + {/* {allList.size > 0 && (
However, we found other sessions based on your search parameters.
@@ -115,7 +110,7 @@ export default class SessionList extends React.PureComponent { >See All
- )} + )} */} } > @@ -139,41 +134,29 @@ export default class SessionList extends React.PureComponent { debounceRequest={1000} /> - {/* - Haven't found the session in the above list?
Try being a bit more specific by setting a specific time frame or simply use different filters - - } - /> */}
); } render() { const { activeTab, allList, total } = this.props; - var filteredList; + // var filteredList; - if (activeTab.type !== ALL && activeTab.type !== 'bookmark' && activeTab.type !== 'live') { // Watchdog sessions - filteredList = allList.filter(session => activeTab.fits(session)) - } else { - filteredList = allList - } + // if (activeTab.type !== ALL && activeTab.type !== 'bookmark' && activeTab.type !== 'live') { // Watchdog sessions + // filteredList = allList.filter(session => activeTab.fits(session)) + // } else { + // filteredList = allList + // } - if (activeTab.type === 'bookmark') { - filteredList = filteredList.filter(item => item.favorite) - } - const _total = activeTab.type === 'all' ? total : filteredList.size + // if (activeTab.type === 'bookmark') { + // filteredList = filteredList.filter(item => item.favorite) + // } + // const _total = activeTab.type === 'all' ? total : allList.size return (
- - { this.renderActiveTabContent(filteredList) } + + { this.renderActiveTabContent(allList) }
); } diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/CustomMetricPercentage.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/CustomMetricPercentage.tsx index 177dccf9a..0dfa6dbd3 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/CustomMetricPercentage.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/CustomMetricPercentage.tsx @@ -12,7 +12,7 @@ function CustomMetriPercentage(props: Props) { return (
{numberWithCommas(data.count)}
-
{`${data.previousCount} ( ${data.countProgress}% )`}
+
{`${parseInt(data.previousCount).toFixed(1)} ( ${parseInt(data.countProgress).toFixed(1)}% )`}
from previous period.
) diff --git a/frontend/app/components/Session/Layout/PlayOverlay.js b/frontend/app/components/Session/Layout/PlayOverlay.js index 537460c26..3fb156172 100644 --- a/frontend/app/components/Session/Layout/PlayOverlay.js +++ b/frontend/app/components/Session/Layout/PlayOverlay.js @@ -1,8 +1,6 @@ import cn from 'classnames'; import { useCallback, useState } from 'react'; - import { Icon } from 'UI'; - import cls from './PlayOverlay.css'; export default function PlayOverlay({ player }) { @@ -11,20 +9,17 @@ export default function PlayOverlay({ player }) { const togglePlay = useCallback(() => { player.togglePlay(); setIconVisible(true); - setTimeout( - () => setIconVisible(false), - 800, - ); + setTimeout(() => setIconVisible(false), 800); }); return (
-
- -
-
+ className="absolute inset-0 flex items-center justify-center" + onClick={ togglePlay } + > +
+ +
+ ); } diff --git a/frontend/app/components/Session/Layout/Player/Timeline.js b/frontend/app/components/Session/Layout/Player/Timeline.js index 5f05834ee..ca1383ffa 100644 --- a/frontend/app/components/Session/Layout/Player/Timeline.js +++ b/frontend/app/components/Session/Layout/Player/Timeline.js @@ -1,12 +1,9 @@ import { useCallback } from 'react'; import cn from 'classnames'; import { Popup } from 'UI'; - import { CRASHES, EVENTS } from 'Player/ios/state'; - import TimeTracker from './TimeTracker'; import PlayerTime from './PlayerTime'; - import cls from './timeline.css'; export default function Timeline({ player }) { @@ -19,7 +16,7 @@ export default function Timeline({ player }) { const time = Math.max(Math.round(p * player.state.endTime), 0); player.jump(time); }); - const scale = 100 / player.state.endTime; + const scale = 100 / player.state.endTime; return (
diff --git a/frontend/app/components/Session_/Player/Controls/Circle.tsx b/frontend/app/components/Session_/Player/Controls/Circle.tsx new file mode 100644 index 000000000..b49d23216 --- /dev/null +++ b/frontend/app/components/Session_/Player/Controls/Circle.tsx @@ -0,0 +1,18 @@ +import React, { memo, FC } from 'react'; +import styles from './timeline.css'; + +interface Props { + preview?: boolean; +} +export const Circle: FC = memo(function Box({ preview }) { + // const backgroundColor = yellow ? 'yellow' : 'white' + return ( +
+ ) + }) + +export default Circle; \ No newline at end of file diff --git a/frontend/app/components/Session_/Player/Controls/Controls.js b/frontend/app/components/Session_/Player/Controls/Controls.js index 22fd3b0cf..ab1baba3e 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.js +++ b/frontend/app/components/Session_/Player/Controls/Controls.js @@ -118,6 +118,7 @@ export default class Controls extends React.Component { componentDidMount() { document.addEventListener('keydown', this.onKeyDown); } + componentWillUnmount() { document.removeEventListener('keydown', this.onKeyDown); //this.props.toggleInspectorMode(false); @@ -166,10 +167,10 @@ export default class Controls extends React.Component { return; } if (this.props.inspectorMode) return; - if (e.key === ' ') { - document.activeElement.blur(); - this.props.togglePlay(); - } + // if (e.key === ' ') { + // document.activeElement.blur(); + // this.props.togglePlay(); + // } if (e.key === 'Esc' || e.key === 'Escape') { this.props.fullscreenOff(); } @@ -262,7 +263,7 @@ export default class Controls extends React.Component { return (
- { !live && } + { !live && } { !fullscreen &&
diff --git a/frontend/app/components/Session_/Player/Controls/CustomDragLayer.tsx b/frontend/app/components/Session_/Player/Controls/CustomDragLayer.tsx new file mode 100644 index 000000000..c72f03ce2 --- /dev/null +++ b/frontend/app/components/Session_/Player/Controls/CustomDragLayer.tsx @@ -0,0 +1,98 @@ +import React, { memo } from 'react'; +import { useDragLayer } from "react-dnd"; +import Circle from './Circle' +import type { CSSProperties, FC } from 'react' + +const layerStyles: CSSProperties = { + position: "fixed", + pointerEvents: "none", + zIndex: 100, + left: 0, + top: 0, + width: "100%", + height: "100%" + }; + +const ItemTypes = { + BOX: 'box', +} + +function getItemStyles(initialOffset, currentOffset, maxX, minX) { + if (!initialOffset || !currentOffset) { + return { + display: "none" + }; + } + let { x, y } = currentOffset; + // if (isSnapToGrid) { + // x -= initialOffset.x; + // y -= initialOffset.y; + // [x, y] = [x, y]; + // x += initialOffset.x; + // y += initialOffset.y; + // } + if (x > maxX) { + x = maxX; + } + + if (x < minX) { + x = minX; + } + const transform = `translate(${x}px, ${initialOffset.y}px)`; + return { + transition: 'transform 0.1s ease-out', + transform, + WebkitTransform: transform + }; +} + +interface Props { + onDrag: (offset: { x: number, y: number } | null) => void; + maxX: number; + minX: number; +} + +const CustomDragLayer: FC = memo(function CustomDragLayer(props) { + const { + itemType, + isDragging, + item, + initialOffset, + currentOffset, + } = useDragLayer((monitor) => ({ + item: monitor.getItem(), + itemType: monitor.getItemType(), + initialOffset: monitor.getInitialSourceClientOffset(), + currentOffset: monitor.getSourceClientOffset(), + isDragging: monitor.isDragging(), + })); + + function renderItem() { + switch (itemType) { + case ItemTypes.BOX: + return ; + default: + return null; + } + } + + if (!isDragging) { + return null; + } + + if (isDragging) { + props.onDrag(currentOffset) + } + + return ( +
+
+ {renderItem()} +
+
+ ); +}) + +export default CustomDragLayer; \ No newline at end of file diff --git a/frontend/app/components/Session_/Player/Controls/DraggableCircle.tsx b/frontend/app/components/Session_/Player/Controls/DraggableCircle.tsx new file mode 100644 index 000000000..9bdf37651 --- /dev/null +++ b/frontend/app/components/Session_/Player/Controls/DraggableCircle.tsx @@ -0,0 +1,67 @@ +import React, { memo, FC, useEffect, useRef, CSSProperties } from 'react'; +import type { DragSourceMonitor } from 'react-dnd' +import { useDrag } from 'react-dnd' +import { getEmptyImage } from 'react-dnd-html5-backend' +import Circle from './Circle' + +function getStyles( + left: number, + isDragging: boolean, + ): CSSProperties { + // const transform = `translate3d(${(left * 1161) / 100}px, -8px, 0)` + return { + position: 'absolute', + top: '-3px', + left: `${left}%`, + // transform, + // WebkitTransform: transform, + // IE fallback: hide the real node using CSS when dragging + // because IE will ignore our custom "empty image" drag preview. + opacity: isDragging ? 0 : 1, + height: isDragging ? 0 : '', + zIndex: '99999', + cursor: 'move' + } +} + +const ItemTypes = { + BOX: 'box', +} + +interface Props { + left: number; + top: number; + onDrop?: (item, monitor) => void; +} + +const DraggableCircle: FC = memo(function DraggableCircle(props) { + const { left, top } = props + const [{ isDragging, item }, dragRef, preview] = useDrag( + () => ({ + type: ItemTypes.BOX, + item: { left, top }, + end: props.onDrop, + collect: (monitor: DragSourceMonitor) => ({ + isDragging: monitor.isDragging(), + item: monitor.getItem(), + }), + }), + [left, top], + ) + + useEffect(() => { + preview(getEmptyImage(), { captureDraggingState: true }) + }, []) + + return ( +
+ +
+ ); +}) + +export default DraggableCircle \ No newline at end of file diff --git a/frontend/app/components/Session_/Player/Controls/TimeTracker.js b/frontend/app/components/Session_/Player/Controls/TimeTracker.js index be91f69fe..e3de669e5 100644 --- a/frontend/app/components/Session_/Player/Controls/TimeTracker.js +++ b/frontend/app/components/Session_/Player/Controls/TimeTracker.js @@ -4,10 +4,6 @@ import styles from './timeTracker.css'; const TimeTracker = ({ time, scale }) => ( -
{ // exception, @@ -51,6 +53,8 @@ const getPointerIcon = (type) => { } @connectPlayer(state => ({ + playing: state.playing, + time: state.time, skipIntervals: state.skipIntervals, events: state.eventList, skip: state.skip, @@ -72,6 +76,11 @@ const getPointerIcon = (type) => { state.getIn([ 'sessions', 'current', 'returningLocationTime' ]), }), { setTimelinePointer }) export default class Timeline extends React.PureComponent { + progressRef = React.createRef() + progressWidth = 0 + seekTime = 0 + wasPlaying = false + seekProgress = (e) => { const { endTime } = this.props; const p = e.nativeEvent.offsetX / e.target.offsetWidth; @@ -88,11 +97,32 @@ export default class Timeline extends React.PureComponent { componentDidMount() { const { issues, events, fetchList, skipToIssue } = this.props; const firstIssue = issues.get(0); + this.progressWidth = this.progressRef.current.offsetWidth; + if (firstIssue && skipToIssue) { this.props.jump(firstIssue.time); } } + onDragEnd = (item, monitor) => { + this.props.jump(this.seekTime); + if (this.wasPlaying) { + this.props.togglePlay(); + } + } + + onDrag = (offset) => { + const { endTime } = this.props; + + const p = (offset.x - 60) / this.progressRef.current.offsetWidth; + const time = Math.max(Math.round(p * endTime), 0); + this.seekTime = time; + if (this.props.playing) { + this.wasPlaying = true; + this.props.pause(); + } + } + render() { const { events, @@ -103,7 +133,7 @@ export default class Timeline extends React.PureComponent { live, logList, exceptionsList, - resourceList, + resourceList, clickRageTime, stackList, fetchList, @@ -111,12 +141,19 @@ export default class Timeline extends React.PureComponent { } = this.props; const scale = 100 / endTime; + return (
{ !live && } -
+
+ + { skip && skipIntervals.map(interval => (
{ + // TODO Find a better way to do this + document.addEventListener('keydown', onKeyDown); + + return () => { + document.removeEventListener('keydown', onKeyDown); + } + }, []) + + const onKeyDown = (e) => { + if (e.key === ' ') { + togglePlayAnimated() + } + } + const togglePlayAnimated = useCallback(() => { setShowPlayOverlayIcon(true); togglePlay(); - setTimeout( - () => setShowPlayOverlayIcon(false), - 800, - ); + setTimeout(() => setShowPlayOverlayIcon(false), 800); }, []); + return (
{ return { type: REFRESH_FILTER_OPTIONS } +} + +export const setScrollPosition = (scrollPosition) => { + return { + type: SET_SCROLL_POSITION, + scrollPosition, + } } \ No newline at end of file diff --git a/frontend/app/initialize.js b/frontend/app/initialize.js index dcfd01058..874bd0586 100644 --- a/frontend/app/initialize.js +++ b/frontend/app/initialize.js @@ -1,13 +1,9 @@ import './init'; - import { render } from 'react-dom'; import { Provider } from 'react-redux'; - import store from './store'; import Router from './Router'; import { StoreProvider, RootStore } from './mstore'; -import { ModalProvider } from './components/Modal'; -import ModalRoot from './components/Modal/ModalRoot'; import { HTML5Backend } from 'react-dnd-html5-backend' import { DndProvider } from 'react-dnd' @@ -17,10 +13,7 @@ document.addEventListener('DOMContentLoaded', () => { - {/* */} - {/* */} - {/* */} diff --git a/frontend/app/svg/icons/pie-chart-fill.svg b/frontend/app/svg/icons/pie-chart-fill.svg index 6aa71eb89..e3e67bb5c 100644 --- a/frontend/app/svg/icons/pie-chart-fill.svg +++ b/frontend/app/svg/icons/pie-chart-fill.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file