import React , { PureComponent } from 'react' ; import PropTypes from 'prop-types' ; import { Action , Dispatch } from 'redux' ; import * as themes from 'redux-devtools-themes' ; import { Base16Theme } from 'redux-devtools-themes' ; import { ActionCreators , LiftedAction , LiftedState , } from '@redux-devtools/core' ; import debounce from 'lodash.debounce' ; import { updateScrollTop , startConsecutiveToggle , LogMonitorAction , } from './actions' ; import reducer , { LogMonitorState } from './reducers' ; import LogMonitorButtonBar from './LogMonitorButtonBar' ; import LogMonitorEntryList from './LogMonitorEntryList' ; const { toggleAction , setActionsActive } = ActionCreators ; const styles = { container : { fontFamily : 'monaco, Consolas, Lucida Console, monospace' , position : 'relative' , overflowY : 'hidden' , width : '100%' , height : '100%' , minWidth : 300 , direction : 'ltr' , } , elements : { position : 'absolute' , left : 0 , right : 0 , top : 0 , bottom : 0 , overflowX : 'hidden' , overflowY : 'auto' , } , } ; interface ExternalProps < S , A extends Action < unknown > > { dispatch : Dispatch < LogMonitorAction | LiftedAction < S , A , LogMonitorState > > ; preserveScrollTop : boolean ; select : ( state : S ) => unknown ; theme : keyof typeof themes | Base16Theme ; expandActionRoot : boolean ; expandStateRoot : boolean ; markStateDiff : boolean ; hideMainButtons ? : boolean ; } interface DefaultProps < S > { select : ( state : unknown ) => unknown ; theme : keyof typeof themes | Base16Theme ; preserveScrollTop : boolean ; expandActionRoot : boolean ; expandStateRoot : boolean ; markStateDiff : boolean ; } export interface LogMonitorProps < S , A extends Action < unknown > > extends LiftedState < S , A , LogMonitorState > { dispatch : Dispatch < LogMonitorAction | LiftedAction < S , A , LogMonitorState > > ; preserveScrollTop : boolean ; select : ( state : S ) => unknown ; theme : keyof typeof themes | Base16Theme ; expandActionRoot : boolean ; expandStateRoot : boolean ; markStateDiff : boolean ; hideMainButtons ? : boolean ; } class LogMonitor < S , A extends Action < unknown > > extends PureComponent < LogMonitorProps < S , A > > { static update = reducer ; static propTypes = { dispatch : PropTypes . func , computedStates : PropTypes . array , actionsById : PropTypes . object , stagedActionIds : PropTypes . array , skippedActionIds : PropTypes . array , monitorState : PropTypes . shape ( { initialScrollTop : PropTypes . number , consecutiveToggleStartId : PropTypes . number , } ) , preserveScrollTop : PropTypes . bool , select : PropTypes . func , theme : PropTypes . oneOfType ( [ PropTypes . object , PropTypes . string ] ) , expandActionRoot : PropTypes . bool , expandStateRoot : PropTypes . bool , markStateDiff : PropTypes . bool , hideMainButtons : PropTypes . bool , } ; static defaultProps : DefaultProps < unknown > = { select : ( state ) => state , theme : 'nicinabox' , preserveScrollTop : true , expandActionRoot : true , expandStateRoot : true , markStateDiff : false , } ; scrollDown ? : boolean ; node ? : HTMLDivElement | null ; updateScrollTop = debounce ( ( ) => { const node = this . node ; this . props . dispatch ( updateScrollTop ( node ? node . scrollTop : 0 ) ) ; } , 500 ) ; scroll ( ) { const node = this . node ; if ( ! node ) { return ; } if ( this . scrollDown ) { const { offsetHeight , scrollHeight } = node ; node . scrollTop = scrollHeight - offsetHeight ; this . scrollDown = false ; } } componentDidMount ( ) { const node = this . node ; if ( ! node || ! this . props . monitorState ) { return ; } if ( this . props . preserveScrollTop ) { node . scrollTop = this . props . monitorState . initialScrollTop ; node . addEventListener ( 'scroll' , this . updateScrollTop ) ; } else { this . scrollDown = true ; this . scroll ( ) ; } } componentWillUnmount ( ) { const node = this . node ; if ( node && this . props . preserveScrollTop ) { node . removeEventListener ( 'scroll' , this . updateScrollTop ) ; } } UNSAFE_componentWillReceiveProps ( nextProps ) { const node = this . node ; if ( ! node ) { this . scrollDown = true ; } else if ( this . props . stagedActionIds . length < nextProps . stagedActionIds . length ) { const { scrollTop , offsetHeight , scrollHeight } = node ; this . scrollDown = Math . abs ( scrollHeight - ( scrollTop + offsetHeight ) ) < 20 ; } else { this . scrollDown = false ; } } componentDidUpdate ( ) { this . scroll ( ) ; } handleToggleAction = ( id ) => { this . props . dispatch ( toggleAction ( id ) ) ; } ; handleToggleConsecutiveAction = ( id ) => { const { monitorState , actionsById } = this . props ; const { consecutiveToggleStartId } = monitorState ; if ( consecutiveToggleStartId && actionsById [ consecutiveToggleStartId ] ) { const { skippedActionIds } = this . props ; const start = Math . min ( consecutiveToggleStartId , id ) ; const end = Math . max ( consecutiveToggleStartId , id ) ; const active = skippedActionIds . indexOf ( consecutiveToggleStartId ) > - 1 ; this . props . dispatch ( setActionsActive ( start , end + 1 , active ) ) ; this . props . dispatch ( startConsecutiveToggle ( null ) ) ; } else if ( id > 0 ) { this . props . dispatch ( startConsecutiveToggle ( id ) ) ; } } ; getTheme ( ) { const { theme } = this . props ; if ( typeof theme !== 'string' ) { return theme ; } if ( typeof themes [ theme ] !== 'undefined' ) { return themes [ theme ] ; } console . warn ( 'DevTools theme ' + theme + ' not found, defaulting to nicinabox' ) ; return themes . nicinabox ; } getRef : React . RefCallback < HTMLDivElement > = ( node ) => { this . node = node ; } ; render ( ) { const theme = this . getTheme ( ) ; const { consecutiveToggleStartId } = this . props . monitorState ; const { dispatch , actionsById , skippedActionIds , stagedActionIds , computedStates , currentStateIndex , select , expandActionRoot , expandStateRoot , markStateDiff , } = this . props ; const entryListProps = { theme , actionsById , skippedActionIds , stagedActionIds , computedStates , currentStateIndex , consecutiveToggleStartId , select , expandActionRoot , expandStateRoot , markStateDiff , onActionClick : this . handleToggleAction , onActionShiftClick : this . handleToggleConsecutiveAction , } ; return ( < div style = { { ... styles . container , backgroundColor : theme . base00 } } > { ! this . props . hideMainButtons && ( < LogMonitorButtonBar theme = { theme } dispatch = { dispatch } hasStates = { computedStates . length > 1 } hasSkippedActions = { skippedActionIds . length > 0 } / > ) } < div style = { this . props . hideMainButtons ? styles . elements : { ... styles . elements , top : 30 } } ref = { this . getRef } > < LogMonitorEntryList { ... entryListProps } / > < /div> < /div>); } } export default LogMonitor as unknown as React . ComponentType < ExternalProps < unknown , Action < unknown > > > & { update ( monitorProps , state , action ) : LogMonitorState ; defaultProps : DefaultProps < unknown > ; } ;