UNPKG

6.36 kBJavaScriptView Raw
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import shouldPureComponentUpdate from 'react-pure-render/function';
4import * as themes from 'redux-devtools-themes';
5import { ActionCreators } from 'redux-devtools';
6import { updateScrollTop, startConsecutiveToggle } from './actions';
7import reducer from './reducers';
8import LogMonitorButtonBar from './LogMonitorButtonBar';
9import LogMonitorEntryList from './LogMonitorEntryList';
10import debounce from 'lodash.debounce';
11
12const { toggleAction, setActionsActive } = ActionCreators;
13
14const styles = {
15 container: {
16 fontFamily: 'monaco, Consolas, Lucida Console, monospace',
17 position: 'relative',
18 overflowY: 'hidden',
19 width: '100%',
20 height: '100%',
21 minWidth: 300,
22 direction: 'ltr'
23 },
24 elements: {
25 position: 'absolute',
26 left: 0,
27 right: 0,
28 top: 0,
29 bottom: 0,
30 overflowX: 'hidden',
31 overflowY: 'auto'
32 }
33};
34
35export default class LogMonitor extends Component {
36 static update = reducer;
37
38 static propTypes = {
39 dispatch: PropTypes.func,
40 computedStates: PropTypes.array,
41 actionsById: PropTypes.object,
42 stagedActionIds: PropTypes.array,
43 skippedActionIds: PropTypes.array,
44 monitorState: PropTypes.shape({
45 initialScrollTop: PropTypes.number,
46 consecutiveToggleStartId: PropTypes.number
47 }),
48
49 preserveScrollTop: PropTypes.bool,
50 select: PropTypes.func,
51 theme: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
52 expandActionRoot: PropTypes.bool,
53 expandStateRoot: PropTypes.bool,
54 markStateDiff: PropTypes.bool,
55 hideMainButtons: PropTypes.bool
56 };
57
58 static defaultProps = {
59 select: state => state,
60 theme: 'nicinabox',
61 preserveScrollTop: true,
62 expandActionRoot: true,
63 expandStateRoot: true,
64 markStateDiff: false
65 };
66
67 shouldComponentUpdate = shouldPureComponentUpdate;
68
69 updateScrollTop = debounce(() => {
70 const node = this.node;
71 this.props.dispatch(updateScrollTop(node ? node.scrollTop : 0));
72 }, 500);
73
74 constructor(props) {
75 super(props);
76 this.handleToggleAction = this.handleToggleAction.bind(this);
77 this.handleToggleConsecutiveAction = this.handleToggleConsecutiveAction.bind(
78 this
79 );
80 this.getRef = this.getRef.bind(this);
81 }
82
83 scroll() {
84 const node = this.node;
85 if (!node) {
86 return;
87 }
88 if (this.scrollDown) {
89 const { offsetHeight, scrollHeight } = node;
90 node.scrollTop = scrollHeight - offsetHeight;
91 this.scrollDown = false;
92 }
93 }
94
95 componentDidMount() {
96 const node = this.node;
97 if (!node || !this.props.monitorState) {
98 return;
99 }
100
101 if (this.props.preserveScrollTop) {
102 node.scrollTop = this.props.monitorState.initialScrollTop;
103 node.addEventListener('scroll', this.updateScrollTop);
104 } else {
105 this.scrollDown = true;
106 this.scroll();
107 }
108 }
109
110 componentWillUnmount() {
111 const node = this.node;
112 if (node && this.props.preserveScrollTop) {
113 node.removeEventListener('scroll', this.updateScrollTop);
114 }
115 }
116
117 UNSAFE_componentWillReceiveProps(nextProps) {
118 const node = this.node;
119 if (!node) {
120 this.scrollDown = true;
121 } else if (
122 this.props.stagedActionIds.length < nextProps.stagedActionIds.length
123 ) {
124 const { scrollTop, offsetHeight, scrollHeight } = node;
125
126 this.scrollDown =
127 Math.abs(scrollHeight - (scrollTop + offsetHeight)) < 20;
128 } else {
129 this.scrollDown = false;
130 }
131 }
132
133 componentDidUpdate() {
134 this.scroll();
135 }
136
137 handleToggleAction(id) {
138 this.props.dispatch(toggleAction(id));
139 }
140
141 handleToggleConsecutiveAction(id) {
142 const { monitorState, actionsById } = this.props;
143 const { consecutiveToggleStartId } = monitorState;
144 if (consecutiveToggleStartId && actionsById[consecutiveToggleStartId]) {
145 const { skippedActionIds } = this.props;
146 const start = Math.min(consecutiveToggleStartId, id);
147 const end = Math.max(consecutiveToggleStartId, id);
148 const active = skippedActionIds.indexOf(consecutiveToggleStartId) > -1;
149 this.props.dispatch(setActionsActive(start, end + 1, active));
150 this.props.dispatch(startConsecutiveToggle(null));
151 } else if (id > 0) {
152 this.props.dispatch(startConsecutiveToggle(id));
153 }
154 }
155
156 getTheme() {
157 let { theme } = this.props;
158 if (typeof theme !== 'string') {
159 return theme;
160 }
161
162 if (typeof themes[theme] !== 'undefined') {
163 return themes[theme];
164 }
165
166 // eslint-disable-next-line no-console
167 console.warn(
168 'DevTools theme ' + theme + ' not found, defaulting to nicinabox'
169 );
170 return themes.nicinabox;
171 }
172
173 getRef(node) {
174 this.node = node;
175 }
176
177 render() {
178 const theme = this.getTheme();
179 const { consecutiveToggleStartId } = this.props.monitorState;
180
181 const {
182 dispatch,
183 actionsById,
184 skippedActionIds,
185 stagedActionIds,
186 computedStates,
187 currentStateIndex,
188 select,
189 expandActionRoot,
190 expandStateRoot,
191 markStateDiff
192 } = this.props;
193
194 const entryListProps = {
195 theme,
196 actionsById,
197 skippedActionIds,
198 stagedActionIds,
199 computedStates,
200 currentStateIndex,
201 consecutiveToggleStartId,
202 select,
203 expandActionRoot,
204 expandStateRoot,
205 markStateDiff,
206 onActionClick: this.handleToggleAction,
207 onActionShiftClick: this.handleToggleConsecutiveAction
208 };
209
210 return (
211 <div style={{ ...styles.container, backgroundColor: theme.base00 }}>
212 {!this.props.hideMainButtons && (
213 <LogMonitorButtonBar
214 theme={theme}
215 dispatch={dispatch}
216 hasStates={computedStates.length > 1}
217 hasSkippedActions={skippedActionIds.length > 0}
218 />
219 )}
220 <div
221 style={
222 this.props.hideMainButtons
223 ? styles.elements
224 : { ...styles.elements, top: 30 }
225 }
226 ref={this.getRef}
227 >
228 <LogMonitorEntryList {...entryListProps} />
229 </div>
230 </div>
231 );
232 }
233}