mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-01-31 11:51:41 +03:00
Import the initial devTools implementation
This commit is contained in:
parent
de43b2e110
commit
644cd6fab6
|
@ -9,9 +9,10 @@
|
|||
"react/jsx-uses-react": 2,
|
||||
"react/jsx-uses-vars": 2,
|
||||
"react/react-in-jsx-scope": 2,
|
||||
"no-console": 0,
|
||||
// Temporarily disabled due to babel-eslint issues:
|
||||
"block-scoped-var": 0,
|
||||
"padded-blocks": 0
|
||||
"padded-blocks": 0,
|
||||
},
|
||||
"plugins": [
|
||||
"react"
|
||||
|
|
55
src/createDevTools.js
Normal file
55
src/createDevTools.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
import createAll from 'react-redux/lib/components/createAll';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { ActionCreators } from './devTools';
|
||||
|
||||
export default function createDevTools(React) {
|
||||
const { PropTypes, Component } = React;
|
||||
const { Provider, Connector } = createAll(React);
|
||||
|
||||
return class DevTools extends Component {
|
||||
static propTypes = {
|
||||
monitor: PropTypes.func.isRequired,
|
||||
store: PropTypes.shape({
|
||||
devToolsStore: PropTypes.shape({
|
||||
dispatch: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
if (props.store && !props.store.devToolsStore) {
|
||||
console.error(
|
||||
'Could not find the devTools store inside your store. ' +
|
||||
'Have you applied devTools() higher-order store?'
|
||||
);
|
||||
}
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { devToolsStore } = this.props.store;
|
||||
return (
|
||||
<Provider store={devToolsStore}>
|
||||
{this.renderRoot}
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
renderRoot = () => {
|
||||
return (
|
||||
<Connector>
|
||||
{this.renderMonitor}
|
||||
</Connector>
|
||||
);
|
||||
};
|
||||
|
||||
renderMonitor = ({ dispatch, ...state }) => {
|
||||
const { monitor: Monitor, ...rest } = this.props;
|
||||
return (
|
||||
<Monitor {...state}
|
||||
{...bindActionCreators(ActionCreators, dispatch)}
|
||||
{...rest} />
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
213
src/devTools.js
Normal file
213
src/devTools.js
Normal file
|
@ -0,0 +1,213 @@
|
|||
const ActionTypes = {
|
||||
PERFORM_ACTION: 'PERFORM_ACTION',
|
||||
RESET: 'RESET',
|
||||
ROLLBACK: 'ROLLBACK',
|
||||
COMMIT: 'COMMIT',
|
||||
SWEEP: 'SWEEP',
|
||||
TOGGLE_ACTION: 'TOGGLE_ACTION'
|
||||
};
|
||||
|
||||
const INIT_ACTION = {
|
||||
type: '@@INIT'
|
||||
};
|
||||
|
||||
function last(arr) {
|
||||
return arr[arr.length - 1];
|
||||
}
|
||||
|
||||
function toggle(obj, key) {
|
||||
const clone = { ...obj };
|
||||
if (clone[key]) {
|
||||
delete clone[key];
|
||||
} else {
|
||||
clone[key] = true;
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the next entry in the log by applying an action.
|
||||
*/
|
||||
function computeNextEntry(reducer, action, state, error) {
|
||||
if (error) {
|
||||
return {
|
||||
state,
|
||||
error: 'Interrupted by an error up the chain'
|
||||
};
|
||||
}
|
||||
|
||||
let nextState = state;
|
||||
let nextError;
|
||||
try {
|
||||
nextState = reducer(state, action);
|
||||
} catch (err) {
|
||||
nextError = err.toString();
|
||||
}
|
||||
|
||||
return {
|
||||
state: nextState,
|
||||
error: nextError
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the reducer on all actions to get a fresh computation log.
|
||||
* It's probably a good idea to do this only if the code has changed,
|
||||
* but until we have some tests we'll just do it every time an action fires.
|
||||
*/
|
||||
function recomputeStates(reducer, committedState, stagedActions, skippedActions) {
|
||||
const computedStates = [];
|
||||
|
||||
for (let i = 0; i < stagedActions.length; i++) {
|
||||
const action = stagedActions[i];
|
||||
|
||||
const previousEntry = computedStates[i - 1];
|
||||
const previousState = previousEntry ? previousEntry.state : committedState;
|
||||
const previousError = previousEntry ? previousEntry.error : undefined;
|
||||
|
||||
const shouldSkip = Boolean(skippedActions[i]);
|
||||
const entry = shouldSkip ?
|
||||
previousEntry :
|
||||
computeNextEntry(reducer, action, previousState, previousError);
|
||||
|
||||
computedStates.push(entry);
|
||||
}
|
||||
|
||||
return computedStates;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Lifts the app state reducer into a DevTools state reducer.
|
||||
*/
|
||||
function liftReducer(reducer, initialState) {
|
||||
const initialLiftedState = {
|
||||
committedState: initialState,
|
||||
stagedActions: [INIT_ACTION],
|
||||
skippedActions: {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages how the DevTools actions modify the DevTools state.
|
||||
*/
|
||||
return function liftedReducer(liftedState = initialLiftedState, liftedAction) {
|
||||
let {
|
||||
committedState,
|
||||
stagedActions,
|
||||
skippedActions,
|
||||
computedStates
|
||||
} = liftedState;
|
||||
|
||||
switch (liftedAction.type) {
|
||||
case ActionTypes.RESET:
|
||||
committedState = initialState;
|
||||
stagedActions = [INIT_ACTION];
|
||||
skippedActions = {};
|
||||
break;
|
||||
case ActionTypes.COMMIT:
|
||||
committedState = last(computedStates).state;
|
||||
stagedActions = [INIT_ACTION];
|
||||
skippedActions = {};
|
||||
break;
|
||||
case ActionTypes.ROLLBACK:
|
||||
stagedActions = [INIT_ACTION];
|
||||
skippedActions = {};
|
||||
break;
|
||||
case ActionTypes.TOGGLE_ACTION:
|
||||
const { index } = liftedAction;
|
||||
skippedActions = toggle(skippedActions, index);
|
||||
break;
|
||||
case ActionTypes.SWEEP:
|
||||
stagedActions = stagedActions.filter((_, i) => !skippedActions[i]);
|
||||
skippedActions = {};
|
||||
break;
|
||||
case ActionTypes.PERFORM_ACTION:
|
||||
const { action } = liftedAction;
|
||||
stagedActions = [...stagedActions, action];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
computedStates = recomputeStates(
|
||||
reducer,
|
||||
committedState,
|
||||
stagedActions,
|
||||
skippedActions
|
||||
);
|
||||
|
||||
return {
|
||||
committedState,
|
||||
stagedActions,
|
||||
skippedActions,
|
||||
computedStates
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifts an app action to a DevTools action.
|
||||
*/
|
||||
function liftAction(action) {
|
||||
const liftedAction = { type: ActionTypes.PERFORM_ACTION, action };
|
||||
return liftedAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlifts the DevTools state to the app state.
|
||||
*/
|
||||
function unliftState(liftedState) {
|
||||
const { computedStates } = liftedState;
|
||||
const { state } = last(computedStates);
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlifts the DevTools store to act like the app's store.
|
||||
*/
|
||||
function unliftStore(liftedStore) {
|
||||
return {
|
||||
...liftedStore,
|
||||
devToolsStore: liftedStore,
|
||||
dispatch(action) {
|
||||
liftedStore.dispatch(liftAction(action));
|
||||
return action;
|
||||
},
|
||||
getState() {
|
||||
return unliftState(liftedStore.getState());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action creators to change the DevTools state.
|
||||
*/
|
||||
export const ActionCreators = {
|
||||
reset() {
|
||||
return { type: ActionTypes.RESET };
|
||||
},
|
||||
rollback() {
|
||||
return { type: ActionTypes.ROLLBACK };
|
||||
},
|
||||
commit() {
|
||||
return { type: ActionTypes.COMMIT };
|
||||
},
|
||||
sweep() {
|
||||
return { type: ActionTypes.SWEEP };
|
||||
},
|
||||
toggleAction(index) {
|
||||
return { type: ActionTypes.TOGGLE_ACTION, index };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Redux DevTools middleware.
|
||||
*/
|
||||
export default function devTools() {
|
||||
return next => (reducer, initialState) => {
|
||||
const liftedReducer = liftReducer(reducer, initialState);
|
||||
const liftedStore = next(liftedReducer);
|
||||
const store = unliftStore(liftedStore);
|
||||
return store;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export { default as devTools } from './devTools';
|
||||
export { default as persistState } from './persistState';
|
39
src/persistState.js
Normal file
39
src/persistState.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
export default function persistState(sessionId) {
|
||||
if (!sessionId) {
|
||||
return next => (...args) => next(...args);
|
||||
}
|
||||
|
||||
return next => (reducer, initialState) => {
|
||||
const key = `redux-dev-session-${sessionId}`;
|
||||
|
||||
let finalInitialState;
|
||||
try {
|
||||
finalInitialState = JSON.parse(localStorage.getItem(key)) || initialState;
|
||||
next(reducer, initialState);
|
||||
} catch (e) {
|
||||
console.warn('Could not read debug session from localStorage:', e);
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} finally {
|
||||
finalInitialState = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const store = next(reducer, finalInitialState);
|
||||
|
||||
return {
|
||||
...store,
|
||||
dispatch(action) {
|
||||
store.dispatch(action);
|
||||
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(store.getState()));
|
||||
} catch (e) {
|
||||
console.warn('Could not write debug session from localStorage:', e);
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
51
src/react/DebugPanel.js
Normal file
51
src/react/DebugPanel.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
|
||||
export function getDefaultStyle(props) {
|
||||
let { left, right, bottom, top } = props;
|
||||
if (typeof left === 'undefined' && typeof right === 'undefined') {
|
||||
right = true;
|
||||
}
|
||||
if (typeof top === 'undefined' && typeof bottom === 'undefined') {
|
||||
bottom = true;
|
||||
}
|
||||
|
||||
return {
|
||||
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'
|
||||
};
|
||||
}
|
||||
|
||||
export default class DebugPanel {
|
||||
static propTypes = {
|
||||
left: PropTypes.bool,
|
||||
right: PropTypes.bool,
|
||||
bottom: PropTypes.bool,
|
||||
top: PropTypes.bool,
|
||||
getStyle: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
getStyle: getDefaultStyle
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={this.props.getStyle(this.props)}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
130
src/react/LogMonitor.js
Normal file
130
src/react/LogMonitor.js
Normal file
|
@ -0,0 +1,130 @@
|
|||
import React, { PropTypes, findDOMNode } from 'react';
|
||||
import LogMonitorEntry from './LogMonitorEntry';
|
||||
|
||||
export default class LogMonitor {
|
||||
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 { 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(
|
||||
<LogMonitorEntry key={i}
|
||||
index={i}
|
||||
select={select}
|
||||
action={action}
|
||||
state={state}
|
||||
collapsed={skippedActions[i]}
|
||||
error={error}
|
||||
onActionClick={::this.handleToggleAction} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
fontFamily: '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>
|
||||
);
|
||||
}
|
||||
}
|
121
src/react/LogMonitorEntry.js
Normal file
121
src/react/LogMonitorEntry.js
Normal file
|
@ -0,0 +1,121 @@
|
|||
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) {
|
||||
const splitToken = token.split('');
|
||||
const finalToken = splitToken.concat(splitToken.reverse());
|
||||
|
||||
const number = finalToken.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 LogMonitorEntry {
|
||||
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) {
|
||||
let errorText = error;
|
||||
if (!errorText) {
|
||||
try {
|
||||
return JSON.stringify(this.props.select(state));
|
||||
} catch (err) {
|
||||
errorText = 'Error selecting state.';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span style={{
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
({errorText})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
handleActionClick() {
|
||||
const { index, onActionClick } = this.props;
|
||||
if (index > 0) {
|
||||
onActionClick(index);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { index, error, action, state, collapsed } = this.props;
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
6
src/react/index.js
Normal file
6
src/react/index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import React from 'react';
|
||||
import createDevTools from '../createDevTools';
|
||||
|
||||
export const DevTools = createDevTools(React);
|
||||
export { default as LogMonitor } from './LogMonitor';
|
||||
export { default as DebugPanel } from './DebugPanel';
|
1
src/react/native/index.js
Normal file
1
src/react/native/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
// TODO
|
Loading…
Reference in New Issue
Block a user