feat(player): connect performance, xray and events to tab state
This commit is contained in:
parent
dcfd6a553d
commit
6cbdb42035
13 changed files with 49 additions and 687 deletions
|
|
@ -1,174 +0,0 @@
|
|||
import React from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import cn from 'classnames';
|
||||
import { Icon, TextEllipsis } from 'UI';
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import { prorata } from 'App/utils';
|
||||
import withOverlay from 'Components/hocs/withOverlay';
|
||||
import LoadInfo from './LoadInfo';
|
||||
import cls from './event.module.css';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
@withOverlay()
|
||||
export default class Event extends React.PureComponent {
|
||||
state = {
|
||||
menuOpen: false,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.wrapper.addEventListener('contextmenu', this.onContextMenu);
|
||||
}
|
||||
|
||||
onContextMenu = (e) => {
|
||||
e.preventDefault();
|
||||
this.setState({ menuOpen: true });
|
||||
}
|
||||
onMouseLeave = () => this.setState({ menuOpen: false })
|
||||
|
||||
copyHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
//const ctrlOrCommandPressed = e.ctrlKey || e.metaKey;
|
||||
//if (ctrlOrCommandPressed && e.keyCode === 67) {
|
||||
const { event } = this.props;
|
||||
copy(event.getIn([ 'target', 'path' ]) || event.url || '');
|
||||
this.setState({ menuOpen: false });
|
||||
}
|
||||
|
||||
toggleInfo = (e) => {
|
||||
e.stopPropagation();
|
||||
this.props.toggleInfo();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
renderBody = () => {
|
||||
const { event } = this.props;
|
||||
let title = event.type;
|
||||
let body;
|
||||
switch (event.type) {
|
||||
case TYPES.LOCATION:
|
||||
title = 'Visited';
|
||||
body = event.url;
|
||||
break;
|
||||
case TYPES.CLICK:
|
||||
title = 'Clicked';
|
||||
body = event.label;
|
||||
break;
|
||||
case TYPES.INPUT:
|
||||
title = 'Input';
|
||||
body = event.value;
|
||||
break;
|
||||
case TYPES.CLICKRAGE:
|
||||
title = `${ event.count } Clicks`;
|
||||
body = event.label;
|
||||
break;
|
||||
case TYPES.IOS_VIEW:
|
||||
title = 'View';
|
||||
body = event.name;
|
||||
break;
|
||||
}
|
||||
const isLocation = event.type === TYPES.LOCATION;
|
||||
const isClickrage = event.type === TYPES.CLICKRAGE;
|
||||
|
||||
return (
|
||||
<div className={ cn(cls.main, 'flex flex-col w-full') } >
|
||||
<div className="flex items-center w-full">
|
||||
{ event.type && <Icon name={`event/${event.type.toLowerCase()}`} size="16" color={isClickrage? 'red' : 'gray-dark' } /> }
|
||||
<div className="ml-3 w-full">
|
||||
<div className="flex w-full items-first justify-between">
|
||||
<div className="flex items-center w-full" style={{ minWidth: '0'}}>
|
||||
<span className={cls.title}>{ title }</span>
|
||||
{/* { body && !isLocation && <div className={ cls.data }>{ body }</div> } */}
|
||||
{ body && !isLocation &&
|
||||
<TextEllipsis maxWidth="60%" className="w-full ml-2 text-sm color-gray-medium" text={body} />
|
||||
}
|
||||
</div>
|
||||
{ isLocation && event.speedIndex != null &&
|
||||
<div className="color-gray-medium flex font-medium items-center leading-none justify-end">
|
||||
<div className="font-size-10 pr-2">{"Speed Index"}</div>
|
||||
<div>{ numberWithCommas(event.speedIndex || 0) }</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{ event.target && event.target.label &&
|
||||
<div className={ cls.badge } >{ event.target.label }</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{ isLocation &&
|
||||
<div className="mt-1">
|
||||
<span className="text-sm font-normal color-gray-medium">{ body }</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
event,
|
||||
selected,
|
||||
isCurrent,
|
||||
onClick,
|
||||
showSelection,
|
||||
onCheckboxClick,
|
||||
showLoadInfo,
|
||||
toggleLoadInfo,
|
||||
isRed,
|
||||
extended,
|
||||
highlight = false,
|
||||
presentInSearch = false,
|
||||
isLastInGroup,
|
||||
whiteBg,
|
||||
} = this.props;
|
||||
const { menuOpen } = this.state;
|
||||
return (
|
||||
<div
|
||||
ref={ ref => { this.wrapper = ref } }
|
||||
onMouseLeave={ this.onMouseLeave }
|
||||
data-openreplay-label="Event"
|
||||
data-type={event.type}
|
||||
className={ cn(cls.event, {
|
||||
[ cls.menuClosed ]: !menuOpen,
|
||||
[ cls.highlighted ]: showSelection ? selected : isCurrent,
|
||||
[ cls.selected ]: selected,
|
||||
[ cls.showSelection ]: showSelection,
|
||||
[ cls.red ]: isRed,
|
||||
[ cls.clickType ]: event.type === TYPES.CLICK,
|
||||
[ cls.inputType ]: event.type === TYPES.INPUT,
|
||||
[ cls.clickrageType ]: event.type === TYPES.CLICKRAGE,
|
||||
[ cls.highlight ] : presentInSearch,
|
||||
[ cls.lastInGroup ]: whiteBg,
|
||||
}) }
|
||||
onClick={ onClick }
|
||||
>
|
||||
{ menuOpen &&
|
||||
<button onClick={ this.copyHandler } className={ cls.contextMenu }>
|
||||
{ event.target ? 'Copy CSS' : 'Copy URL' }
|
||||
</button>
|
||||
}
|
||||
<div className={ cls.topBlock }>
|
||||
<div className={ cls.firstLine }>
|
||||
{ this.renderBody() }
|
||||
</div>
|
||||
{/* { event.type === TYPES.LOCATION &&
|
||||
<div className="text-sm font-normal color-gray-medium">{event.url}</div>
|
||||
} */}
|
||||
</div>
|
||||
{ event.type === TYPES.LOCATION && (event.fcpTime || event.visuallyComplete || event.timeToInteractive) &&
|
||||
<LoadInfo
|
||||
showInfo={ showLoadInfo }
|
||||
onClick={ toggleLoadInfo }
|
||||
event={ event }
|
||||
prorata={ prorata({
|
||||
parts: 100,
|
||||
elements: { a: event.fcpTime, b: event.visuallyComplete, c: event.timeToInteractive },
|
||||
startDivisorFn: elements => elements / 1.2,
|
||||
// eslint-disable-next-line no-mixed-operators
|
||||
divisorFn: (elements, parts) => elements / (2 * parts + 1),
|
||||
}) }
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux'
|
||||
import { TextEllipsis } from 'UI';
|
||||
import withToggle from 'HOCs/withToggle';
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import Event from './Event'
|
||||
import stl from './eventGroupWrapper.module.css';
|
||||
import NoteEvent from './NoteEvent';
|
||||
import { setEditNoteTooltip } from 'Duck/sessions';;
|
||||
|
||||
// TODO: incapsulate toggler in LocationEvent
|
||||
@withToggle('showLoadInfo', 'toggleLoadInfo')
|
||||
@connect(
|
||||
(state) => ({
|
||||
members: state.getIn(['members', 'list']),
|
||||
currentUserId: state.getIn(['user', 'account', 'id']),
|
||||
}),
|
||||
{ setEditNoteTooltip }
|
||||
)
|
||||
class EventGroupWrapper extends React.Component {
|
||||
toggleLoadInfo = (e) => {
|
||||
e.stopPropagation();
|
||||
this.props.toggleLoadInfo();
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.showLoadInfo !== this.props.showLoadInfo ||
|
||||
prevProps.query !== this.props.query ||
|
||||
prevProps.event.timestamp !== this.props.event.timestamp ||
|
||||
prevProps.isNote !== this.props.isNote
|
||||
) {
|
||||
this.props.mesureHeight();
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.toggleLoadInfo(this.props.isFirst);
|
||||
this.props.mesureHeight();
|
||||
}
|
||||
|
||||
onEventClick = (e) => this.props.onEventClick(e, this.props.event);
|
||||
|
||||
onCheckboxClick = (e) => this.props.onCheckboxClick(e, this.props.event);
|
||||
|
||||
render() {
|
||||
const {
|
||||
event,
|
||||
isLastEvent,
|
||||
isLastInGroup,
|
||||
isSelected,
|
||||
isCurrent,
|
||||
isEditing,
|
||||
showSelection,
|
||||
showLoadInfo,
|
||||
isFirst,
|
||||
presentInSearch,
|
||||
isNote,
|
||||
filterOutNote,
|
||||
} = this.props;
|
||||
const isLocation = event.type === TYPES.LOCATION;
|
||||
|
||||
const whiteBg =
|
||||
(isLastInGroup && event.type !== TYPES.LOCATION) ||
|
||||
(!isLastEvent && event.type !== TYPES.LOCATION);
|
||||
const safeRef = String(event.referrer || '');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
stl.container,
|
||||
'!py-1',
|
||||
{
|
||||
[stl.last]: isLastInGroup,
|
||||
[stl.first]: event.type === TYPES.LOCATION,
|
||||
[stl.dashAfter]: isLastInGroup && !isLastEvent,
|
||||
},
|
||||
isLastInGroup && '!pb-2',
|
||||
event.type === TYPES.LOCATION && '!pt-2 !pb-2'
|
||||
)}
|
||||
>
|
||||
{isFirst && isLocation && event.referrer && (
|
||||
<div className={stl.referrer}>
|
||||
<TextEllipsis>
|
||||
Referrer: <span className={stl.url}>{safeRef}</span>
|
||||
</TextEllipsis>
|
||||
</div>
|
||||
)}
|
||||
{isNote ? (
|
||||
<NoteEvent
|
||||
note={event}
|
||||
filterOutNote={filterOutNote}
|
||||
onEdit={this.props.setEditNoteTooltip}
|
||||
noEdit={this.props.currentUserId !== event.userId}
|
||||
/>
|
||||
) : isLocation ? (
|
||||
<Event
|
||||
extended={isFirst}
|
||||
key={event.key}
|
||||
event={event}
|
||||
onClick={this.onEventClick}
|
||||
selected={isSelected}
|
||||
showLoadInfo={showLoadInfo}
|
||||
toggleLoadInfo={this.toggleLoadInfo}
|
||||
isCurrent={isCurrent}
|
||||
presentInSearch={presentInSearch}
|
||||
isLastInGroup={isLastInGroup}
|
||||
whiteBg={whiteBg}
|
||||
/>
|
||||
) : (
|
||||
<Event
|
||||
key={event.key}
|
||||
event={event}
|
||||
onClick={this.onEventClick}
|
||||
onCheckboxClick={this.onCheckboxClick}
|
||||
selected={isSelected}
|
||||
isCurrent={isCurrent}
|
||||
showSelection={showSelection}
|
||||
overlayed={isEditing}
|
||||
presentInSearch={presentInSearch}
|
||||
isLastInGroup={isLastInGroup}
|
||||
whiteBg={whiteBg}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EventGroupWrapper
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import { List, AutoSizer, CellMeasurer } from "react-virtualized";
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import { setEventFilter, filterOutNote } from 'Duck/sessions';
|
||||
import EventGroupWrapper from './EventGroupWrapper';
|
||||
import styles from './eventsBlock.module.css';
|
||||
import EventSearch from './EventSearch/EventSearch';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { RootStore } from 'App/duck'
|
||||
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache'
|
||||
import { InjectedEvent } from 'Types/session/event'
|
||||
import Session from 'Types/session'
|
||||
|
||||
interface IProps {
|
||||
setEventFilter: (filter: { query: string }) => void
|
||||
filteredEvents: InjectedEvent[]
|
||||
setActiveTab: (tab?: string) => void
|
||||
query: string
|
||||
events: Session['events']
|
||||
notesWithEvents: Session['notesWithEvents']
|
||||
filterOutNote: (id: string) => void
|
||||
eventsIndex: number[]
|
||||
}
|
||||
|
||||
function EventsBlock(props: IProps) {
|
||||
const [mouseOver, setMouseOver] = React.useState(true)
|
||||
const scroller = React.useRef<List>(null)
|
||||
const cache = useCellMeasurerCache( {
|
||||
fixedWidth: true,
|
||||
defaultHeight: 300
|
||||
});
|
||||
|
||||
const { store, player } = React.useContext(PlayerContext)
|
||||
|
||||
const { eventListNow, playing } = store.get()
|
||||
|
||||
const {
|
||||
filteredEvents,
|
||||
eventsIndex,
|
||||
filterOutNote,
|
||||
query,
|
||||
setActiveTab,
|
||||
events,
|
||||
notesWithEvents,
|
||||
} = props
|
||||
|
||||
const currentTimeEventIndex = eventListNow.length > 0 ? eventListNow.length - 1 : 0
|
||||
const usedEvents = filteredEvents || notesWithEvents
|
||||
|
||||
const write = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
props.setEventFilter({ query: value })
|
||||
|
||||
setTimeout(() => {
|
||||
if (!scroller.current) return;
|
||||
|
||||
scroller.current.scrollToRow(0);
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const clearSearch = () => {
|
||||
props.setEventFilter({ query: '' })
|
||||
if (scroller.current) {
|
||||
scroller.current.forceUpdateGrid();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (!scroller.current) return;
|
||||
|
||||
scroller.current.scrollToRow(0);
|
||||
}, 100)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
clearSearch()
|
||||
}
|
||||
}, [])
|
||||
React.useEffect(() => {
|
||||
if (scroller.current) {
|
||||
scroller.current.forceUpdateGrid();
|
||||
if (!mouseOver) {
|
||||
scroller.current.scrollToRow(currentTimeEventIndex);
|
||||
}
|
||||
}
|
||||
}, [currentTimeEventIndex])
|
||||
|
||||
const onEventClick = (_: React.MouseEvent, event: { time: number }) => player.jump(event.time)
|
||||
const onMouseOver = () => setMouseOver(true)
|
||||
const onMouseLeave = () => setMouseOver(false)
|
||||
|
||||
const renderGroup = ({ index, key, style, parent }: { index: number; key: string; style: React.CSSProperties; parent: any }) => {
|
||||
const isLastEvent = index === usedEvents.length - 1;
|
||||
const isLastInGroup = isLastEvent || usedEvents[index + 1]?.type === TYPES.LOCATION;
|
||||
const event = usedEvents[index];
|
||||
const isNote = 'noteId' in event
|
||||
const isCurrent = index === currentTimeEventIndex;
|
||||
|
||||
const heightBug = index === 0 && event?.type === TYPES.LOCATION && 'referrer' in event ? { top: 2 } : {}
|
||||
return (
|
||||
<CellMeasurer
|
||||
key={key}
|
||||
cache={cache}
|
||||
parent={parent}
|
||||
rowIndex={index}
|
||||
>
|
||||
{({measure, registerChild}) => (
|
||||
<div style={{ ...style, ...heightBug }} ref={registerChild}>
|
||||
<EventGroupWrapper
|
||||
query={query}
|
||||
presentInSearch={eventsIndex.includes(index)}
|
||||
isFirst={index==0}
|
||||
mesureHeight={measure}
|
||||
onEventClick={ onEventClick }
|
||||
event={ event }
|
||||
isLastEvent={ isLastEvent }
|
||||
isLastInGroup={ isLastInGroup }
|
||||
isCurrent={ isCurrent }
|
||||
showSelection={ !playing }
|
||||
isNote={isNote}
|
||||
filterOutNote={filterOutNote}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
);
|
||||
}
|
||||
|
||||
const isEmptySearch = query && (usedEvents.length === 0 || !usedEvents)
|
||||
return (
|
||||
<>
|
||||
<div className={ cn(styles.header, 'p-4') }>
|
||||
<div className={ cn(styles.hAndProgress, 'mt-3') }>
|
||||
<EventSearch
|
||||
onChange={write}
|
||||
setActiveTab={setActiveTab}
|
||||
value={query}
|
||||
header={
|
||||
<div className="text-xl">User Events <span className="color-gray-medium">{ events.length }</span></div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={ cn("flex-1 px-4 pb-4", styles.eventsList) }
|
||||
id="eventList"
|
||||
data-openreplay-masked
|
||||
onMouseOver={ onMouseOver }
|
||||
onMouseLeave={ onMouseLeave }
|
||||
>
|
||||
{isEmptySearch && (
|
||||
<div className='flex items-center'>
|
||||
<Icon name="binoculars" size={18} />
|
||||
<span className='ml-2'>No Matching Results</span>
|
||||
</div>
|
||||
)}
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<List
|
||||
ref={scroller}
|
||||
className={ styles.eventsList }
|
||||
height={height + 10}
|
||||
width={248}
|
||||
overscanRowCount={6}
|
||||
itemSize={230}
|
||||
rowCount={usedEvents.length}
|
||||
deferredMeasurementCache={cache}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowRenderer={renderGroup}
|
||||
scrollToAlignment="start"
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect((state: RootStore) => ({
|
||||
session: state.getIn([ 'sessions', 'current' ]),
|
||||
notesWithEvents: state.getIn([ 'sessions', 'current' ]).notesWithEvents,
|
||||
events: state.getIn([ 'sessions', 'current' ]).events,
|
||||
filteredEvents: state.getIn([ 'sessions', 'filteredEvents' ]),
|
||||
query: state.getIn(['sessions', 'eventsQuery']),
|
||||
eventsIndex: state.getIn([ 'sessions', 'eventsIndex' ]),
|
||||
}), {
|
||||
setEventFilter,
|
||||
filterOutNote
|
||||
})(observer(EventsBlock))
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import React from 'react';
|
||||
import styles from './loadInfo.module.css';
|
||||
import { numberWithCommas } from 'App/utils'
|
||||
|
||||
const LoadInfo = ({ showInfo = false, onClick, event: { fcpTime, visuallyComplete, timeToInteractive }, prorata: { a, b, c } }) => (
|
||||
<div>
|
||||
<div className={ styles.bar } onClick={ onClick }>
|
||||
{ typeof fcpTime === 'number' && <div style={ { width: `${ a }%` } } /> }
|
||||
{ typeof visuallyComplete === 'number' && <div style={ { width: `${ b }%` } } /> }
|
||||
{ typeof timeToInteractive === 'number' && <div style={ { width: `${ c }%` } } /> }
|
||||
</div>
|
||||
<div className={ styles.bottomBlock } data-hidden={ !showInfo }>
|
||||
{ typeof fcpTime === 'number' &&
|
||||
<div className={ styles.wrapper }>
|
||||
<div className={ styles.lines } />
|
||||
<div className={ styles.label } >{ 'Time to Render' }</div>
|
||||
<div className={ styles.value }>{ `${ numberWithCommas(fcpTime || 0) }ms` }</div>
|
||||
</div>
|
||||
}
|
||||
{ typeof visuallyComplete === 'number' &&
|
||||
<div className={ styles.wrapper }>
|
||||
<div className={ styles.lines } />
|
||||
<div className={ styles.label } >{ 'Visually Complete' }</div>
|
||||
<div className={ styles.value }>{ `${ numberWithCommas(visuallyComplete || 0) }ms` }</div>
|
||||
</div>
|
||||
}
|
||||
{ typeof timeToInteractive === 'number' &&
|
||||
<div className={ styles.wrapper }>
|
||||
<div className={ styles.lines } />
|
||||
<div className={ styles.label } >{ 'Time To Interactive' }</div>
|
||||
<div className={ styles.value }>{ `${ numberWithCommas(timeToInteractive || 0) }ms` }</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
LoadInfo.displayName = 'LoadInfo';
|
||||
|
||||
export default LoadInfo;
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import { tagProps, Note } from 'App/services/NotesService';
|
||||
import { formatTimeOrDate } from 'App/date';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { ItemMenu } from 'UI';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { toast } from 'react-toastify';
|
||||
import { session } from 'App/routes';
|
||||
import { confirm } from 'UI';
|
||||
import { TeamBadge } from 'Shared/SessionListContainer/components/Notes';
|
||||
|
||||
interface Props {
|
||||
note: Note;
|
||||
noEdit: boolean;
|
||||
filterOutNote: (id: number) => void;
|
||||
onEdit: (noteTooltipObj: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
function NoteEvent(props: Props) {
|
||||
const { settingsStore, notesStore } = useStore();
|
||||
const { timezone } = settingsStore.sessionSettings;
|
||||
|
||||
const onEdit = () => {
|
||||
props.onEdit({
|
||||
isVisible: true,
|
||||
isEdit: true,
|
||||
time: props.note.timestamp,
|
||||
note: {
|
||||
timestamp: props.note.timestamp,
|
||||
tag: props.note.tag,
|
||||
isPublic: props.note.isPublic,
|
||||
message: props.note.message,
|
||||
sessionId: props.note.sessionId,
|
||||
noteId: props.note.noteId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onCopy = () => {
|
||||
copy(
|
||||
`${window.location.origin}/${window.location.pathname.split('/')[1]}${session(
|
||||
props.note.sessionId
|
||||
)}${props.note.timestamp > 0 ? `?jumpto=${props.note.timestamp}¬e=${props.note.noteId}` : `?note=${props.note.noteId}`}`
|
||||
);
|
||||
toast.success('Note URL copied to clipboard');
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to delete this note?`,
|
||||
})
|
||||
) {
|
||||
notesStore.deleteNote(props.note.noteId).then((r) => {
|
||||
props.filterOutNote(props.note.noteId);
|
||||
toast.success('Note deleted');
|
||||
});
|
||||
}
|
||||
};
|
||||
const menuItems = [
|
||||
{ icon: 'pencil', text: 'Edit', onClick: onEdit, disabled: props.noEdit },
|
||||
{ icon: 'link-45deg', text: 'Copy URL', onClick: onCopy },
|
||||
{ icon: 'trash', text: 'Delete', onClick: onDelete },
|
||||
];
|
||||
return (
|
||||
<div
|
||||
className="flex items-start flex-col p-2 border rounded"
|
||||
style={{ background: '#FFFEF5' }}
|
||||
>
|
||||
<div className="flex items-center w-full relative">
|
||||
<div className="p-3 bg-gray-light rounded-full">
|
||||
<Icon name="quotes" color="main" />
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
<div
|
||||
className="text-base"
|
||||
style={{
|
||||
maxWidth: 150,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{props.note.userName}
|
||||
</div>
|
||||
<div className="text-disabled-text text-sm">
|
||||
{formatTimeOrDate(props.note.createdAt as unknown as number, timezone)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="cursor-pointer absolute" style={{ right: -5 }}>
|
||||
<ItemMenu bold items={menuItems} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="text-base capitalize-first my-3 overflow-y-scroll overflow-x-hidden"
|
||||
style={{ maxHeight: 200, maxWidth: 220 }}
|
||||
>
|
||||
{props.note.message}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 flex-wrap w-full">
|
||||
{props.note.tag ? (
|
||||
<div
|
||||
key={props.note.tag}
|
||||
style={{
|
||||
// @ts-ignore
|
||||
background: tagProps[props.note.tag],
|
||||
userSelect: 'none',
|
||||
padding: '1px 6px',
|
||||
}}
|
||||
className="rounded-full text-white text-xs select-none w-fit"
|
||||
>
|
||||
{props.note.tag}
|
||||
</div>
|
||||
) : null}
|
||||
{!props.note.isPublic ? null : <TeamBadge />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(NoteEvent);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './EventsBlock';
|
||||
|
|
@ -29,14 +29,14 @@ interface IProps {
|
|||
function EventsBlock(props: IProps) {
|
||||
const [mouseOver, setMouseOver] = React.useState(true);
|
||||
const scroller = React.useRef<List>(null);
|
||||
const cache = useCellMeasurerCache( {
|
||||
const cache = useCellMeasurerCache({
|
||||
fixedWidth: true,
|
||||
defaultHeight: 300,
|
||||
});
|
||||
|
||||
const { store, player } = React.useContext(PlayerContext);
|
||||
|
||||
const { eventListNow, playing } = store.get();
|
||||
const { playing, tabStates } = store.get();
|
||||
|
||||
const {
|
||||
filteredEvents,
|
||||
|
|
@ -44,10 +44,14 @@ function EventsBlock(props: IProps) {
|
|||
filterOutNote,
|
||||
query,
|
||||
setActiveTab,
|
||||
events,
|
||||
notesWithEvents,
|
||||
} = props;
|
||||
|
||||
// TODO! multitab tab id
|
||||
const eventListNow = Object.values(tabStates).reduce((acc: any[], tab) => {
|
||||
return acc.concat(tab.eventListNow)
|
||||
}, [])
|
||||
|
||||
const currentTimeEventIndex = eventListNow.length > 0 ? eventListNow.length - 1 : 0;
|
||||
const usedEvents = filteredEvents || notesWithEvents;
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import stl from './performance.module.css';
|
|||
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import InfoLine from '../BottomBlock/InfoLine';
|
||||
import { toJS } from "mobx";
|
||||
|
||||
const CPU_VISUAL_OFFSET = 10;
|
||||
|
||||
|
|
@ -183,17 +184,22 @@ function Performance({
|
|||
const [_data, setData] = React.useState<any[]>([])
|
||||
|
||||
const {
|
||||
performanceChartTime,
|
||||
performanceChartData,
|
||||
connType,
|
||||
connBandwidth,
|
||||
performanceAvailability: availability,
|
||||
tabStates,
|
||||
currentTab,
|
||||
} = store.get();
|
||||
|
||||
React.useState(() => {
|
||||
const {
|
||||
performanceChartTime = [],
|
||||
performanceChartData = [],
|
||||
performanceAvailability: availability = {}
|
||||
} = tabStates[currentTab];
|
||||
|
||||
React.useEffect(() => {
|
||||
setTicks(generateTicks(performanceChartData));
|
||||
setData(addFpsMetadata(performanceChartData));
|
||||
})
|
||||
}, [currentTab])
|
||||
|
||||
|
||||
const onDotClick = ({ index: pointer }: { index: number }) => {
|
||||
|
|
|
|||
|
|
@ -151,11 +151,16 @@ function NetworkPanel({ startedAt }: { startedAt: number }) {
|
|||
domContentLoadedTime,
|
||||
loadTime,
|
||||
domBuildingTime,
|
||||
fetchList,
|
||||
resourceList,
|
||||
fetchListNow,
|
||||
resourceListNow,
|
||||
tabStates,
|
||||
currentTab
|
||||
} = store.get()
|
||||
const {
|
||||
fetchList = [],
|
||||
resourceList = [],
|
||||
fetchListNow = [],
|
||||
resourceListNow = []
|
||||
} = tabStates[currentTab]
|
||||
|
||||
const { showModal } = useModal();
|
||||
const [sortBy, setSortBy] = useState('time');
|
||||
const [sortAscending, setSortAscending] = useState(true);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,12 @@ const TABS = TAB_KEYS.map((tab) => ({ text: tab, key: tab }))
|
|||
function StackEventPanel() {
|
||||
const { player, store } = React.useContext(PlayerContext)
|
||||
const jump = (t: number) => player.jump(t)
|
||||
const { stackList: list, stackListNow: listNow } = store.get()
|
||||
const { currentTab, tabStates } = store.get()
|
||||
|
||||
const {
|
||||
stackList: list = [],
|
||||
stackListNow: listNow = [],
|
||||
} = tabStates[currentTab]
|
||||
|
||||
const {
|
||||
sessionStore: { devTools },
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import useCancelableTimeout from 'App/hooks/useCancelableTimeout'
|
|||
const TIMEOUT_DURATION = 5000;
|
||||
|
||||
export function getLastItemTime(...lists: Timed[][]) {
|
||||
console.log(lists)
|
||||
return Math.max(...lists.map(l => l.length ? l[l.length-1].time : 0))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,9 +64,7 @@ export const visualChanges = [
|
|||
export default class MessageManager {
|
||||
static INITIAL_STATE: State = {
|
||||
...SCREEN_INITIAL_STATE,
|
||||
tabStates: {
|
||||
'': { ...TabSessionManager.INITIAL_STATE },
|
||||
},
|
||||
tabStates: {},
|
||||
skipIntervals: [],
|
||||
error: false,
|
||||
ready: false,
|
||||
|
|
@ -125,6 +123,7 @@ export default class MessageManager {
|
|||
this.activityManager.end()
|
||||
this.state.update({ skipIntervals: this.activityManager.list })
|
||||
}
|
||||
Object.values(this.tabs).forEach(tab => tab.onFileReadSuccess?.())
|
||||
}
|
||||
|
||||
public onFileReadFailed = (e: any) => {
|
||||
|
|
@ -176,7 +175,7 @@ export default class MessageManager {
|
|||
this.activeTab = tabId
|
||||
}
|
||||
if (!this.tabs[this.activeTab]) {
|
||||
console.log(this.tabs, this.activeTab, tabId, this.activeTabManager.list)
|
||||
console.error('missing tab state', this.tabs, this.activeTab, tabId, this.activeTabManager.list)
|
||||
}
|
||||
// console.log(this.tabs, this.activeTab)
|
||||
this.tabs[this.activeTab].move(t)
|
||||
|
|
@ -187,7 +186,7 @@ export default class MessageManager {
|
|||
}
|
||||
}
|
||||
|
||||
public changeTab(tabId) {
|
||||
public changeTab(tabId: string) {
|
||||
this.activeTab = tabId
|
||||
this.state.update({ currentTab: tabId })
|
||||
this.tabs[tabId].move(this.state.get().time)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import ListWalker from "Player/common/ListWalker";
|
||||
import {
|
||||
ConnectionInformation,
|
||||
Message, MType,
|
||||
Message, MType, ResourceTiming,
|
||||
SetPageLocation,
|
||||
SetViewportScroll,
|
||||
SetViewportSize
|
||||
|
|
@ -22,13 +22,20 @@ import { isDOMType } from "Player/web/messages/filters.gen";
|
|||
export interface TabState extends ListsState {
|
||||
performanceAvailability?: PerformanceTrackManager['availability']
|
||||
performanceChartData: PerformanceChartPoint[],
|
||||
performanceChartTime: PerformanceChartPoint[]
|
||||
cssLoading: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* DO NOT DELETE UNUSED METHODS
|
||||
* THEY'RE ALL USED IN MESSAGE MANAGER VIA this.tabs[id]
|
||||
* */
|
||||
|
||||
export default class TabSessionManager {
|
||||
static INITIAL_STATE: TabState = {
|
||||
...LISTS_INITIAL_STATE,
|
||||
performanceChartData: [],
|
||||
performanceChartTime: [],
|
||||
cssLoading: false,
|
||||
}
|
||||
|
||||
|
|
@ -155,7 +162,7 @@ export default class TabSessionManager {
|
|||
case MType.ResourceTiming:
|
||||
// TODO: merge `resource` and `fetch` lists into one here instead of UI
|
||||
if (msg.initiator !== ResourceType.FETCH && msg.initiator !== ResourceType.XHR) {
|
||||
this.lists.lists.resource.insert(getResourceFromResourceTiming(msg, this.sessionStart))
|
||||
this.lists.lists.resource.insert(getResourceFromResourceTiming(msg as ResourceTiming, this.sessionStart))
|
||||
}
|
||||
break;
|
||||
case MType.Fetch:
|
||||
|
|
@ -211,7 +218,7 @@ export default class TabSessionManager {
|
|||
}
|
||||
|
||||
move(t: number, index?: number): void {
|
||||
const stateToUpdate: Partial<State> = {};
|
||||
const stateToUpdate: Record<string, any> = {};
|
||||
/* == REFACTOR_ME == */
|
||||
const lastLoadedLocationMsg = this.loadedLocationManager.moveGetLast(t, index);
|
||||
if (!!lastLoadedLocationMsg) {
|
||||
|
|
@ -300,7 +307,8 @@ export default class TabSessionManager {
|
|||
}
|
||||
|
||||
public onFileReadSuccess = () => {
|
||||
const stateToUpdate : Partial<State>= {
|
||||
console.log('triggered', this.performanceTrackManager)
|
||||
const stateToUpdate : Partial<Record<string,any>> = {
|
||||
performanceChartData: this.performanceTrackManager.chartData,
|
||||
performanceAvailability: this.performanceTrackManager.availability,
|
||||
...this.lists.getFullListsState(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue