Skip to content

Implement Redux DevTools #234

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 15 commits into from
3 changes: 2 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"stage": 0,
"loose": "all"
"loose": "all",
"plugins": ["object-assign"]
}
29 changes: 23 additions & 6 deletions examples/counter/containers/App.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import React, { Component } from 'react';
import CounterApp from './CounterApp';
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import * as reducers from '../reducers';

import devTools from '../redux-devtools/index';
import persistState from '../redux-devtools/persistState';
import DebugPanel from '../redux-devtools/DebugPanel';
import ReduxMonitor from '../redux-devtools/ReduxMonitor';

// TODO: move into a separate project
function thunk({ dispatch, getState }) {
return next => action =>
Expand All @@ -12,16 +17,28 @@ function thunk({ dispatch, getState }) {
next(action);
}

const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
const finalCreateStore = compose(
applyMiddleware(),
devTools(),
persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)),
createStore
);

const reducer = combineReducers(reducers);
const store = createStoreWithMiddleware(reducer);
const store = finalCreateStore(combineReducers(reducers));

export default class App extends Component {
render() {
return (
<Provider store={store}>
{() => <CounterApp />}
</Provider>
<div>
<Provider store={store}>
{() => <CounterApp />}
</Provider>

<DebugPanel top right bottom>
<ReduxMonitor store={store} />
</DebugPanel>
</div>
);
}
}
46 changes: 46 additions & 0 deletions examples/counter/redux-devtools/DebugPanel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { PropTypes } from 'react';

export default class DebugPanel {
static propTypes = {
left: PropTypes.bool,
right: PropTypes.bool,
bottom: PropTypes.bool,
top: PropTypes.bool
};

render() {
if (process.env.NODE_ENV === 'production') {
return null;
}

let { left, right, bottom, top } = this.props;
if (typeof left === 'undefined' && typeof right === 'undefined') {
right = true;
}
if (typeof top === 'undefined' && typeof bottom === 'undefined') {
bottom = true;
}

return (
<div style={{
position: 'fixed',
zIndex: 999,
fontSize: 17,
overflow: 'scroll',
opacity: 0.92,
background: 'black',
color: 'white',
padding: '1em',
left: left ? 0 : undefined,
right: right ? 0 : undefined,
top: top ? 0 : undefined,
bottom: bottom ? 0 : undefined,
maxHeight: (bottom && top) ? '100%' : '20%',
maxWidth: (left && right) ? '100%' : '20%',
wordWrap: 'break-word'
}}>
{this.props.children}
</div>
);
}
}
119 changes: 119 additions & 0 deletions examples/counter/redux-devtools/Entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, { PropTypes } from 'react';

function hsvToRgb(h, s, v) {
const i = Math.floor(h);
const f = h - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
const mod = i % 6;
const r = [v, q, p, p, t, v][mod];
const g = [t, v, v, q, p, p][mod];
const b = [p, p, t, v, v, q][mod];

return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}

function colorFromString(token) {
token = token.split('');
token = token.concat(token.reverse());
const number = token
.reduce((sum, char) => sum + char.charCodeAt(0), 0) *
Math.abs(Math.sin(token.length));

const h = Math.round((number * (180 / Math.PI) * token.length) % 360);
const s = number % 100 / 100;
const v = 1;

return hsvToRgb(h, s, v);
}

export default class Entry {
static propTypes = {
index: PropTypes.number.isRequired,
state: PropTypes.object.isRequired,
action: PropTypes.object.isRequired,
select: PropTypes.func.isRequired,
error: PropTypes.string,
onActionClick: PropTypes.func.isRequired,
collapsed: PropTypes.bool
};

printState(state, error) {
if (!error) {
try {
return JSON.stringify(this.props.select(state));
} catch (err) {
error = 'Error selecting state.';
}
}

return (
<span style={{
fontStyle: 'italic'
}}>
({error})
</span>
);
}

handleActionClick(e) {
const { index, onActionClick } = this.props;
if (index > 0) {
onActionClick(index);
}
}

render() {
const { index, error, action, state, collapsed, onActionClick } = this.props;
const { type = '' } = action;
const { r, g, b } = colorFromString(action.type);

return (
<div style={{
textDecoration: collapsed ? 'line-through' : 'none'
}}>
<a onClick={::this.handleActionClick}
style={{
opacity: collapsed ? 0.5 : 1,
marginTop: '1em',
display: 'block',
paddingBottom: '1em',
paddingTop: '1em',
color: `rgb(${r}, ${g}, ${b})`,
cursor: (index > 0) ? 'hand' : 'default',
WebkitUserSelect: 'none'
}}>
{JSON.stringify(action)}
</a>

{!collapsed &&
<p style={{
textAlign: 'center',
transform: 'rotate(180deg)'
}}>
</p>
}

{!collapsed &&
<div style={{
paddingBottom: '1em',
paddingTop: '1em',
color: 'lightyellow'
}}>
{this.printState(state, error)}
</div>
}

<hr style={{
marginBottom: '2em'
}} />
</div>
);
}
}
130 changes: 130 additions & 0 deletions examples/counter/redux-devtools/Monitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import React, { PropTypes, findDOMNode } from 'react';
import Entry from './Entry';

export default class Monitor {
static propTypes = {
computedStates: PropTypes.array.isRequired,
stagedActions: PropTypes.array.isRequired,
skippedActions: PropTypes.object.isRequired,
reset: PropTypes.func.isRequired,
commit: PropTypes.func.isRequired,
rollback: PropTypes.func.isRequired,
sweep: PropTypes.func.isRequired,
toggleAction: PropTypes.func.isRequired,
select: PropTypes.func.isRequired
};

static defaultProps = {
select: (state) => state
};

componentWillReceiveProps(nextProps) {
if (this.props.stagedActions.length < nextProps.stagedActions.length) {
const scrollableNode = findDOMNode(this).parentElement;
const { scrollTop, offsetHeight, scrollHeight } = scrollableNode;

this.scrollDown = Math.abs(
scrollHeight - (scrollTop + offsetHeight)
) < 20;
} else {
this.scrollDown = false;
}
}

componentDidUpdate(prevProps) {
if (
prevProps.stagedActions.length < this.props.stagedActions.length &&
this.scrollDown
) {
const scrollableNode = findDOMNode(this).parentElement;
const { scrollTop, offsetHeight, scrollHeight } = scrollableNode;

scrollableNode.scrollTop = scrollHeight - offsetHeight;
this.scrollDown = false;
}
}

handleRollback() {
this.props.rollback();
}

handleSweep() {
this.props.sweep();
}

handleCommit() {
this.props.commit();
}

handleToggleAction(index) {
this.props.toggleAction(index);
}

handleReset() {
this.props.reset();
}

render() {
const elements = [];
const { skippedActions, stagedActions, computedStates, select } = this.props;

for (let i = 0; i < stagedActions.length; i++) {
const action = stagedActions[i];
const { state, error } = computedStates[i];

elements.push(
<Entry key={i}
index={i}
select={select}
action={action}
state={state}
collapsed={skippedActions[i]}
error={error}
onActionClick={::this.handleToggleAction} />
);
}

return (
<div style={{
fontFamily: 'Consolas, monospace',
position: 'relative'
}}>
<div>
<a onClick={::this.handleReset}
style={{ textDecoration: 'underline', cursor: 'hand' }}>
Reset
</a>
</div>
{elements}
<div>
{computedStates.length > 1 &&
<a onClick={::this.handleRollback}
style={{ textDecoration: 'underline', cursor: 'hand' }}>
Rollback
</a>
}
{Object.keys(skippedActions).some(key => skippedActions[key]) &&
<span>
{' • '}
<a onClick={::this.handleSweep}
style={{ textDecoration: 'underline', cursor: 'hand' }}>
Sweep
</a>
</span>
}
{computedStates.length > 1 &&
<span>
<span>
{' • '}
</span>
<a onClick={::this.handleCommit}
style={{ textDecoration: 'underline', cursor: 'hand' }}>
Commit
</a>
</span>
}
</div>
</div>
);
}
}
41 changes: 41 additions & 0 deletions examples/counter/redux-devtools/ReduxMonitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import { Provider, Connector } from 'react-redux';
import { ActionCreators } from './index';
import Monitor from './Monitor';

export default class ReduxMonitor {
static propTypes = {
store: PropTypes.shape({
devToolsStore: PropTypes.shape({
dispatch: PropTypes.func.isRequired
}).isRequired
}).isRequired,
select: PropTypes.func
};

render() {
const { devToolsStore } = this.props.store;
return (
<Provider store={devToolsStore}>
{this.renderRoot}
</Provider>
);
}

renderRoot = () => {
return (
<Connector>
{this.renderMonitor}
</Connector>
);
};

renderMonitor = ({ dispatch, ...props }) => {
return (
<Monitor {...props}
{...bindActionCreators(ActionCreators, dispatch)}
select={this.props.select} />
);
};
}
Loading