change(ui) - player timeline slider and other fixes

This commit is contained in:
Shekar Siri 2022-04-12 12:38:23 +02:00
parent 410ffb1475
commit fa7ae29a62
18 changed files with 327 additions and 88 deletions

View file

@ -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 (
<NoContent
@ -105,7 +100,7 @@ export default class SessionList extends React.PureComponent {
subtext={
<div>
<div>Please try changing your search parameters.</div>
{allList.size > 0 && (
{/* {allList.size > 0 && (
<div className="pt-2">
However, we found other sessions based on your search parameters.
<div>
@ -115,7 +110,7 @@ export default class SessionList extends React.PureComponent {
>See All</Button>
</div>
</div>
)}
)} */}
</div>
}
>
@ -139,41 +134,29 @@ export default class SessionList extends React.PureComponent {
debounceRequest={1000}
/>
</div>
{/* <LoadMoreButton
className="mt-12 mb-12"
displayedCount={displayedCount}
totalCount={list.size}
loading={loading}
onClick={this.addPage}
description={ displayedCount === list.size &&
<div className="color-gray-medium text-sm text-center my-3">
Haven't found the session in the above list? <br/>Try being a bit more specific by setting a specific time frame or simply use different filters
</div>
}
/> */}
</NoContent>
);
}
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 (
<div className="">
<SessionListHeader activeTab={activeTab} count={_total}/>
{ this.renderActiveTabContent(filteredList) }
<SessionListHeader activeTab={activeTab} count={total}/>
{ this.renderActiveTabContent(allList) }
</div>
);
}

View file

@ -12,7 +12,7 @@ function CustomMetriPercentage(props: Props) {
return (
<div className="flex flex-col items-center justify-center" style={{ height: '240px'}}>
<div className="text-6xl">{numberWithCommas(data.count)}</div>
<div className="text-lg mt-6">{`${data.previousCount} ( ${data.countProgress}% )`}</div>
<div className="text-lg mt-6">{`${parseInt(data.previousCount).toFixed(1)} ( ${parseInt(data.countProgress).toFixed(1)}% )`}</div>
<div className="color-gray-medium">from previous period.</div>
</div>
)

View file

@ -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 (
<div
className="absolute inset-0 flex items-center justify-center"
onClick={ togglePlay }
>
<div className={ cn("flex items-center justify-center", cls.iconWrapper, { [ cls.zoomWrapper ]: iconVisible }) } >
<Icon name={ player.state.playing ? "play" : "pause"} size="30" color="gray-medium"/>
</div>
</div>
className="absolute inset-0 flex items-center justify-center"
onClick={ togglePlay }
>
<div className={ cn("flex items-center justify-center", cls.iconWrapper, { [ cls.zoomWrapper ]: iconVisible }) } >
<Icon name={ player.state.playing ? "play" : "pause"} size="30" color="gray-medium"/>
</div>
</div>
);
}

View file

@ -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 (
<div className="flex items-center" >
<PlayerTime player={player} timeKey="time"/>

View file

@ -0,0 +1,18 @@
import React, { memo, FC } from 'react';
import styles from './timeline.css';
interface Props {
preview?: boolean;
}
export const Circle: FC<Props> = memo(function Box({ preview }) {
// const backgroundColor = yellow ? 'yellow' : 'white'
return (
<div
className={ styles.positionTracker }
// style={ { left: `${ time * scale }%` } }
role={preview ? 'BoxPreview' : 'Box'}
/>
)
})
export default Circle;

View file

@ -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 (
<div className={ cn(styles.controls, {'px-5 pt-0' : live}) }>
{ !live && <Timeline jump={ this.props.jump } /> }
{ !live && <Timeline jump={ this.props.jump } pause={this.props.pause} togglePlay={this.props.togglePlay} /> }
{ !fullscreen &&
<div className={ styles.buttons } data-is-live={ live }>
<div>

View file

@ -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<Props> = 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 <Circle />;
default:
return null;
}
}
if (!isDragging) {
return null;
}
if (isDragging) {
props.onDrag(currentOffset)
}
return (
<div style={layerStyles}>
<div
style={getItemStyles(initialOffset, currentOffset, props.maxX, props.minX)}
>
{renderItem()}
</div>
</div>
);
})
export default CustomDragLayer;

View file

@ -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<Props> = 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 (
<div
ref={dragRef}
style={getStyles(left, isDragging)}
role="DraggableBox"
>
<Circle />
</div>
);
})
export default DraggableCircle

View file

@ -4,10 +4,6 @@ import styles from './timeTracker.css';
const TimeTracker = ({ time, scale }) => (
<React.Fragment>
<div
className={ styles.positionTracker }
style={ { left: `${ time * scale }%` } }
/>
<span
className={ styles.playedTimeline }
style={ { width: `${ time * scale }%` } }

View file

@ -8,6 +8,8 @@ import { ReduxTime } from './Time';
import stl from './timeline.css';
import { TYPES } from 'Types/session/event';
import { setTimelinePointer } from 'Duck/sessions';
import DraggableCircle from './DraggableCircle';
import CustomDragLayer from './CustomDragLayer';
const getPointerIcon = (type) => {
// 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 (
<div
className={ cn("flex items-center") }
>
{ !live && <ReduxTime name="time" /> }
<div className={ stl.progress } onClick={ disabled ? null : this.seekProgress }>
<div
className={ stl.progress }
onClick={ disabled ? null : this.seekProgress }
ref={ this.progressRef }
>
<DraggableCircle left={this.props.time * scale} onDrop={this.onDragEnd} />
<CustomDragLayer onDrag={this.onDrag} minX={70} maxX={this.progressRef.current && this.progressRef.current.offsetWidth + 70} />
<TimeTracker scale={ scale } />
{ skip && skipIntervals.map(interval =>
(<div

View file

@ -2,4 +2,6 @@
.time {
padding: 0 12px;
color: $gray-medium;
width: 70px;
text-align: center;
}

View file

@ -1,4 +1,29 @@
@import 'zindex.css';
.positionTracker {
width: 15px;
height: 15px;
/* border: solid 1px $teal; */
outline: solid 1px $teal;
outline-style: inset;
margin-left: -7px;
border-radius: 50%;
background-color: $active-blue;
position: absolute;
left: 0;
z-index: $positionTracker;
top: 0;
transition: all 0.2s ease-out;
&:hover,
&:focus {
transition: all 0.1s ease-in;
width: 20px;
height: 20px;
top: -2px;
left: -2px;
}
}
.progress {
height: 10px;

View file

@ -1,4 +1,4 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import cn from 'classnames';
import { Icon } from 'UI';
@ -12,14 +12,28 @@ interface Props {
export default function PlayIconLayer({ playing, togglePlay }: Props) {
const [ showPlayOverlayIcon, setShowPlayOverlayIcon ] = useState(false);
useEffect(() => {
// 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 (
<div className={ clsOv.overlay } onClick={ togglePlayAnimated }>
<div

View file

@ -45,6 +45,8 @@ export default class Player extends React.PureComponent {
closedLive,
} = this.props;
console.log('PlayerControls', PlayerControls)
return (
<div
className={ cn(className, stl.playerBody, "flex flex-col relative") }

View file

@ -1,6 +1,6 @@
.wrapper {
position: relative;
/* margin-left: 25px; */
margin-left: 15px;
&:hover .pin {
border: solid thin rgba(0,0,0,0.2);

View file

@ -30,6 +30,7 @@ const APPLY = `${name}/APPLY`;
const SET_ALERT_METRIC_ID = `${name}/SET_ALERT_METRIC_ID`;
const UPDATE_CURRENT_PAGE = `${name}/UPDATE_CURRENT_PAGE`;
const SET_ACTIVE_TAB = `${name}/SET_ACTIVE_TAB`;
const SET_SCROLL_POSITION = `${name}/SET_SCROLL_POSITION`;
const REFRESH_FILTER_OPTIONS = 'filters/REFRESH_FILTER_OPTIONS';
@ -53,6 +54,7 @@ const initialState = Map({
filterSearchList: {},
currentPage: 1,
activeTab: {name: 'All', type: 'all' },
scrollY: 0,
});
// Metric - Series - [] - filters
@ -91,6 +93,8 @@ function reducer(state = initialState, action = {}) {
return state.set('currentPage', action.page);
case SET_ACTIVE_TAB:
return state.set('activeTab', action.tab).set('currentPage', 1);
case SET_SCROLL_POSITION:
return state.set('scrollY', action.scrollPosition);
}
return state;
}
@ -300,4 +304,11 @@ export const refreshFilterOptions = () => {
return {
type: REFRESH_FILTER_OPTIONS
}
}
export const setScrollPosition = (scrollPosition) => {
return {
type: SET_SCROLL_POSITION,
scrollPosition,
}
}

View file

@ -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', () => {
<Provider store={ store }>
<StoreProvider store={new RootStore()}>
<DndProvider backend={HTML5Backend}>
{/* <ModalProvider> */}
{/* <ModalRoot /> */}
<Router />
{/* </ModalProvider> */}
</DndProvider>
</StoreProvider>
</Provider>

View file

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pie-chart-fill" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-pie-chart-fill" viewBox="0 0 16 16">
<path d="M15.985 8.5H8.207l-5.5 5.5a8 8 0 0 0 13.277-5.5zM2 13.292A8 8 0 0 1 7.5.015v7.778l-5.5 5.5zM8.5.015V7.5h7.485A8.001 8.001 0 0 0 8.5.015z"/>
</svg>

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 247 B