openreplay/frontend/app/components/Session_/Storage/Storage.tsx
Delirium a71381da40
getting rid of redux for good (#2556)
* start moving ui to redux tlk

* remove unused reducer

* changes for gdpr and site types

* ui: migrating duck/roles to mobx

* ui: drop unreferenced types

* ui: drop unreferenced types

* ui: move player slice reducer to mobx family

* ui: move assignments to issueReportingStore.ts

* remove issues store

* some fixes after issues store

* remove errors reducer, drop old components

* finish removing errors reducer

* start moving integrations state to mobx

* change(ui): funnel duck cleanup

* change(ui): custom fields

* change(ui): customMetrics cleanup

* change(ui): customMetrics cleanup

* change(ui): duck/filters minor cleanup

* change(ui): duck/filters cleanup

* change(ui): duck/customMetrics cleanup and upgrades

* fix integrations service, fix babel config to >.25 + not ie

* refactoring integrations reducers etc WIP

* finish removing integrations state

* some fixes for integrated check

* start of projects refactoring

* move api and "few" files to new project store

* new batch for site -> projects

* fix setid context

* move all critical components, drop site duck

* remove all duck/site refs, remove old components

* fixup for SessionTags.tsx, remove duck/sources (?)

* move session store

* init sessionstore outside of context

* fix userfilter

* replace simple actions for session store

* sessions sotre

* Rtm temp (#2597)

* change(ui): duck/search wip

* change(ui): duck/search wip

* change(ui): duck/search wip

* change(ui): duck/searchLive wip

* change(ui): duck/searchLive wip

* change(ui): duck/searchLive wip

* change(ui): duck/searchLive wip

* change(ui): search states

* change(ui): search states

* change(ui): search states

* change(ui): fix savedSearch store

* change(ui): fix savedSearch store

* some fixes for session connector

* change(ui): fix savedSearch store

* change(ui): fix searchLive

* change(ui): fix searchLive

* fixes for session replay

* change(ui): bookmark fetch

* last components for sessions

* add fetchautoplaylist

* finish session reducer, remove deleted reducers

* change(ui): fix the search fetch

* change(ui): fix the search fetch

* fix integrations call ctx

* ensure ctx for sessionstore

* fix(ui): checking for latest sessions path

* start removing user reducer

* removing user reducer pt2...

* finish user store

* remove rand log

* fix crashes

* tinkering workflow file for tracker test

* making sure prefetched sessions work properly

* fix conflict

* fix router redirects during loading

---------

Co-authored-by: Shekar Siri <sshekarsiri@gmail.com>
2024-10-03 11:38:36 +02:00

387 lines
No EOL
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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'
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<HTMLButtonElement>();
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<string, any>, prevItem?: Record<string, any>) => {
if (!showDiffs) {
return;
}
if (!prevItem) {
// we don't have state before first action
return <div style={{ flex: 3 }} className="p-1" />;
}
const stateDiff = diff(prevItem.state, item?.state);
if (!stateDiff) {
return (
<div style={{ flex: 3 }} className="flex flex-col p-2 pr-0 font-mono text-disabled-text">
No diff
</div>
);
}
return (
<div style={{ flex: 3 }} className="flex flex-col p-1 font-mono">
{stateDiff.map((d: Record<string, any>, i: number) => renderDiffs(d, i))}
</div>
);
};
const renderDiffs = (diff: Record<string, any>, i: number) => {
const path = diff.path.join('.')
return (
<React.Fragment key={i}>
<DiffRow path={path} diff={diff} />
</React.Fragment>
);
};
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<string, any>, i: number, prevItem?: Record<string, any>) => {
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 (
<div
className={cn('flex justify-between items-start', src !== null ? 'border-b' : '')}
key={`store-${i}`}
>
{src === null ? (
<div className="font-mono" style={{ flex: 2, marginLeft: '26.5%' }}>
{name}
</div>
) : (
<>
{renderDiff(itemD, prevItemD)}
<div style={{ flex: 2 }} className={cn("flex pt-2", showDiffs && 'pl-10')}>
<JSONTree
name={ensureString(name)}
src={src}
collapsed
collapseStringsAfterLength={7}
/>
</div>
</>
)}
<div
style={{ flex: 1 }}
className="flex-1 flex gap-2 pt-2 items-center justify-end self-start"
>
{typeof item?.duration === 'number' && (
<div className="font-size-12 color-gray-medium">{formatMs(itemD.duration)}</div>
)}
<div className="w-12">
{i + 1 < listNow.length && (
<button className={stl.button} onClick={() => player.jump(item.time)}>
{'JUMP'}
</button>
)}
{i + 1 === listNow.length && i + 1 < list.length && (
<button className={stl.button} ref={lastBtnRef} onClick={goNext}>
{'NEXT'}
</button>
)}
</div>
</div>
</div>
);
};
if (type === STORAGE_TYPES.REDUX) {
return <ReduxViewer />
}
return (
<BottomBlock>
{/*@ts-ignore*/}
<>
<BottomBlock.Header>
{list.length > 0 && (
<div className="flex w-full">
<h3 style={{ width: '25%', marginRight: 20 }} className="font-semibold">
{'STATE'}
</h3>
{showDiffs ? (
<h3 style={{ width: '39%' }} className="font-semibold">
DIFFS
</h3>
) : null}
<h3 style={{ width: '30%' }} className="font-semibold">
{getActionsName(type)}
</h3>
<h3 style={{ paddingRight: 30, marginLeft: 'auto' }} className="font-semibold">
<Tooltip title="Time to execute">TTE</Tooltip>
</h3>
</div>
)}
</BottomBlock.Header>
<BottomBlock.Content className="flex">
<NoContent
title="Nothing to display yet"
subtext={
!hintIsHidden ? (
<>
{
'Inspect your application state while youre replaying your users sessions. OpenReplay supports '
}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/redux"
target="_blank"
>
Redux
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/vuex"
target="_blank"
>
VueX
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/pinia"
target="_blank"
>
Pinia
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/zustand"
target="_blank"
>
Zustand
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/mobx"
target="_blank"
>
MobX
</a>
{' and '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/ngrx"
target="_blank"
>
NgRx
</a>
.
<br />
<br />
<button className="color-teal" onClick={() => hideHint('storage')}>
Got It!
</button>
</>
) : null
}
size="small"
show={list.length === 0}
>
<div className="ph-10 scroll-y" style={{ width: '25%' }}>
{list.length === 0 ? (
<div className="color-gray-light font-size-16 mt-20 text-center">
{'Empty state.'}
</div>
) : (
<JSONTree collapsed={2} src={stateObject}
/>
)}
</div>
<div className="flex" style={{ width: '75%' }}>
<Autoscroll className="ph-10">
{decodedList.map((item: Record<string, any>, i: number) =>
renderItem(item, i, i > 0 ? decodedList[i - 1] : undefined)
)}
</Autoscroll>
</div>
</NoContent>
</BottomBlock.Content>
</>
</BottomBlock>
);
}
export default observer(Storage);
/**
* TODO: compute diff and only decode the required parts
* WIP example
* function useStorageDecryptedList(list: Record<string, any>[], 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])
* }
*
* */