import React from 'react'; import { connect } from 'react-redux'; import { hideHint } from 'Duck/components/player'; import { connectPlayer, selectStorageType, STORAGE_TYPES, selectStorageListNow, selectStorageList, } from 'Player'; import { JSONTree, NoContent, Tooltip } from 'UI'; import { formatMs } from 'App/date'; import { diff } from 'deep-diff'; import { jump } from 'Player'; import BottomBlock from '../BottomBlock/index'; import DiffRow from './DiffRow'; import stl from './storage.module.css'; import { List, CellMeasurer, CellMeasurerCache, AutoSizer } from 'react-virtualized'; const ROW_HEIGHT = 90; function getActionsName(type) { switch (type) { case STORAGE_TYPES.MOBX: case STORAGE_TYPES.VUEX: return 'MUTATIONS'; default: return 'ACTIONS'; } } @connectPlayer((state) => ({ type: selectStorageType(state), list: selectStorageList(state), listNow: selectStorageListNow(state), })) @connect( (state) => ({ hintIsHidden: state.getIn(['components', 'player', 'hiddenHints', 'storage']), }), { hideHint, } ) export default class Storage extends React.PureComponent { constructor(props) { super(props); this.lastBtnRef = React.createRef(); this._list = React.createRef(); this.cache = new CellMeasurerCache({ fixedWidth: true, keyMapper: (index) => this.props.listNow[index], }); this._rowRenderer = this._rowRenderer.bind(this); } focusNextButton() { if (this.lastBtnRef.current) { this.lastBtnRef.current.focus(); } } componentDidMount() { this.focusNextButton(); } componentDidUpdate(prevProps, prevState) { if (prevProps.listNow.length !== this.props.listNow.length) { this.focusNextButton(); /** possible performance gain, but does not work with dynamic list insertion for some reason * getting NaN offsets, maybe I detect changed rows wrongly */ // const newRows = this.props.listNow.filter(evt => prevProps.listNow.indexOf(evt._index) < 0); // if (newRows.length > 0) { // const newRowsIndexes = newRows.map(r => this.props.listNow.indexOf(r)) // newRowsIndexes.forEach(ind => this.cache.clear(ind)) // this._list.recomputeRowHeights(newRowsIndexes) // } } } renderDiff(item, prevItem) { if (!prevItem) { // we don't have state before first action return
; } const stateDiff = diff(prevItem.state, item.state); if (!stateDiff) { return (
No diff
); } return (
{stateDiff.map((d, i) => this.renderDiffs(d, i))}
); } renderDiffs(diff, i) { const path = this.createPath(diff); return ( ); } createPath = (diff) => { let path = []; if (diff.path) { path = path.concat(diff.path); } if (typeof diff.index !== 'undefined') { path.push(diff.index); } const pathStr = path.length ? path.join('.') : ''; return pathStr; }; ensureString(actionType) { if (typeof actionType === 'string') return actionType; return 'UNKNOWN'; } goNext = () => { const { list, listNow } = this.props; jump(list[listNow.length].time, list[listNow.length]._index); }; renderTab() { const { listNow } = this.props; if (listNow.length === 0) { return 'Not initialized'; //? } return ; } renderItem(item, i, prevItem, style) { const { type } = this.props; let src; let name; switch (type) { case STORAGE_TYPES.REDUX: case STORAGE_TYPES.NGRX: src = item.action; name = src && src.type; break; case STORAGE_TYPES.VUEX: src = item.mutation; name = src && src.type; break; case STORAGE_TYPES.MOBX: src = item.payload; name = `@${item.type} ${src && src.type}`; break; case STORAGE_TYPES.ZUSTAND: src = null; name = item.mutation.join(''); } return (
{src === null ? (
{name}
) : ( <> {this.renderDiff(item, prevItem, i)}
)}
{typeof item.duration === 'number' && (
{formatMs(item.duration)}
)}
{i + 1 < this.props.listNow.length && ( )} {i + 1 === this.props.listNow.length && i + 1 < this.props.list.length && ( )}
); } _rowRenderer({ index, parent, key, style }) { const { listNow } = this.props; if (!listNow[index]) return console.warn(index, listNow); return ( {this.renderItem(listNow[index], index, index > 0 ? listNow[index - 1] : undefined, style)} ); } render() { const { type, list, listNow, hintIsHidden } = this.props; const showStore = type !== STORAGE_TYPES.MOBX; return ( {list.length > 0 && (
{showStore && (

{'STATE'}

)} {type !== STORAGE_TYPES.ZUSTAND ? (

DIFFS

) : null}

{getActionsName(type)}

TTE

)}
{ 'Inspect your application state while you’re replaying your users sessions. OpenReplay supports ' } Redux {', '} VueX {', '} Pinia {', '} Zustand {', '} MobX {' and '} NgRx .

) : null } size="small" show={listNow.length === 0} > {showStore && (
{listNow.length === 0 ? (
{'Empty state.'}
) : ( this.renderTab() )}
)}
{({ height, width }) => ( { this._list = element; }} deferredMeasurementCache={this.cache} overscanRowCount={1} rowCount={Math.ceil(parseInt(this.props.listNow.length) || 1)} rowHeight={ROW_HEIGHT} rowRenderer={this._rowRenderer} width={width} height={height} /> )}
); } }