1 | import React, { Component } from 'react';
|
2 | import PropTypes from 'prop-types';
|
3 | import shouldPureComponentUpdate from 'react-pure-render/function';
|
4 | import * as themes from 'redux-devtools-themes';
|
5 | import { ActionCreators } from 'redux-devtools';
|
6 | import { updateScrollTop, startConsecutiveToggle } from './actions';
|
7 | import reducer from './reducers';
|
8 | import LogMonitorButtonBar from './LogMonitorButtonBar';
|
9 | import LogMonitorEntryList from './LogMonitorEntryList';
|
10 | import debounce from 'lodash.debounce';
|
11 |
|
12 | const { toggleAction, setActionsActive } = ActionCreators;
|
13 |
|
14 | const 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 |
|
35 | export 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 |
|
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 | }
|