import React from 'react'; import { connect } from 'react-redux'; import { hideHint } from 'Duck/components/player'; import { PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; import { JSONTree, NoContent, Tooltip } from 'UI'; import { formatMs } from 'App/date'; import diff from 'microdiff' import { STORAGE_TYPES, selectStorageList, selectStorageListNow, selectStorageType } from 'Player'; import Autoscroll from '../Autoscroll'; import BottomBlock from '../BottomBlock/index'; import DiffRow from './DiffRow'; import cn from 'classnames'; import stl from './storage.module.css'; import logger from "App/logger"; import ReduxViewer from './ReduxViewer' function getActionsName(type: string) { switch (type) { case STORAGE_TYPES.MOBX: case STORAGE_TYPES.VUEX: return 'MUTATIONS'; default: return 'ACTIONS'; } } const storageDecodeKeys = { [STORAGE_TYPES.REDUX]: ['state', 'action'], [STORAGE_TYPES.NGRX]: ['state', 'action'], [STORAGE_TYPES.VUEX]: ['state', 'mutation'], [STORAGE_TYPES.ZUSTAND]: ['state', 'mutation'], [STORAGE_TYPES.MOBX]: ['payload'], [STORAGE_TYPES.NONE]: ['state, action', 'payload', 'mutation'], } interface Props { hideHint: (args: string) => void; hintIsHidden: boolean; } function Storage(props: Props) { const lastBtnRef = React.useRef(); const [showDiffs, setShowDiffs] = React.useState(false); const [stateObject, setState] = React.useState({}); const { player, store } = React.useContext(PlayerContext); const { tabStates, currentTab } = store.get() const state = tabStates[currentTab] || {} const listNow = selectStorageListNow(state) || []; const list = selectStorageList(state) || []; const type = selectStorageType(state) || STORAGE_TYPES.NONE React.useEffect(() => { let currentState; if (listNow.length === 0) { currentState = decodeMessage(list[0]) } else { currentState = decodeMessage(listNow[listNow.length - 1]) } const stateObj = currentState?.state || currentState?.payload?.state || {} const newState = Object.assign(stateObject, stateObj); setState(newState); }, [listNow.length]); const decodeMessage = (msg: any) => { const decoded = {}; const pureMSG = { ...msg } const keys = storageDecodeKeys[type]; try { keys.forEach(key => { if (pureMSG[key]) { // @ts-ignore TODO: types for decoder decoded[key] = player.decodeMessage(pureMSG[key]); } }); } catch (e) { logger.error("Error on message decoding: ", e, pureMSG); return null; } return { ...pureMSG, ...decoded }; } const decodedList = React.useMemo(() => { return listNow.map(msg => { return decodeMessage(msg) }) }, [listNow.length]) const focusNextButton = () => { if (lastBtnRef.current) { lastBtnRef.current.focus(); } }; React.useEffect(() => { focusNextButton(); }, []); React.useEffect(() => { focusNextButton(); }, [listNow]); const renderDiff = (item: Record, prevItem?: Record) => { if (!showDiffs) { return; } 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: Record, i: number) => renderDiffs(d, i))}
); }; const renderDiffs = (diff: Record, i: number) => { const path = diff.path.join('.') return ( ); }; const ensureString = (actionType: string) => { if (typeof actionType === 'string') return actionType; return 'UNKNOWN'; }; const goNext = () => { // , list[listNow.length]._index player.jump(list[listNow.length].time); }; const renderItem = (item: Record, i: number, prevItem?: Record) => { let src; let name; const itemD = item const prevItemD = prevItem ? prevItem : undefined switch (type) { case STORAGE_TYPES.REDUX: case STORAGE_TYPES.NGRX: src = itemD?.action; name = src && src.type; break; case STORAGE_TYPES.VUEX: src = itemD?.mutation; name = src && src.type; break; case STORAGE_TYPES.MOBX: src = itemD?.payload; name = `@${item.type} ${src && src.type}`; break; case STORAGE_TYPES.ZUSTAND: src = null; name = itemD?.mutation.join(''); } if (src !== null && !showDiffs && itemD?.state) { setShowDiffs(true); } return (
{src === null ? (
{name}
) : ( <> {renderDiff(itemD, prevItemD)}
)}
{typeof item?.duration === 'number' && (
{formatMs(itemD.duration)}
)}
{i + 1 < listNow.length && ( )} {i + 1 === listNow.length && i + 1 < list.length && ( )}
); }; const { hintIsHidden } = props; if (type === STORAGE_TYPES.REDUX) { return } return ( {/*@ts-ignore*/} <> {list.length > 0 && (

{'STATE'}

{showDiffs ? (

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={list.length === 0} >
{list.length === 0 ? (
{'Empty state.'}
) : ( )}
{decodedList.map((item: Record, i: number) => renderItem(item, i, i > 0 ? decodedList[i - 1] : undefined) )}
); } export default connect( (state: any) => ({ hintIsHidden: state.getIn(['components', 'player', 'hiddenHints', 'storage']), }), { hideHint, } )(observer(Storage)); /** * TODO: compute diff and only decode the required parts * WIP example * function useStorageDecryptedList(list: Record[], type: string, player: IWebPlayer) { * const [decryptedList, setDecryptedList] = React.useState(list); * const [listLength, setLength] = React.useState(list.length) * * const decodeMessage = (msg: any, type: StorageType) => { * const decoded = {}; * const pureMSG = { ...msg } * const keys = storageDecodeKeys[type]; * try { * keys.forEach(key => { * if (pureMSG[key]) { * // @ts-ignore TODO: types for decoder * decoded[key] = player.decodeMessage(pureMSG[key]); * } * }); * } catch (e) { * logger.error("Error on message decoding: ", e, pureMSG); * return null; * } * return { ...pureMSG, ...decoded }; * } * * React.useEffect(() => { * if (list.length !== listLength) { * const last = list[list.length - 1]._index; * let diff; * if (last < decryptedList[decryptedList.length - 1]._index) { * * } * diff = list.filter(item => !decryptedList.includes(i => i._index === item._index)) * const decryptedDiff = diff.map(item => { * return player.decodeMessage(item) * }) * const result = * } * }, [list.length]) * } * * */