import React from 'react'; import { useStore } from 'App/mstore'; 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'; import { Segmented } from 'antd' 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'], }; function Storage() { const { uiPlayerStore } = useStore(); const hintIsHidden = uiPlayerStore.hiddenHints.storage; const hideHint = uiPlayerStore.hideHint; 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 && ( )}
); }; if (type === STORAGE_TYPES.REDUX) { return ; } return ( {/*@ts-ignore*/} <>

{'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 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]) * } * * */