Move redux-devtools-inspector package (#429)

* Move from zalmoxisus/remotedev-inspector-monitor fork

* Fix linting

* Add credits

* Upgrade to react 16

Moved from zalmoxisus/remotedev-inspector-monitor/pull/5

* Upgrade dependences

* Add demo for ES6 map

From
alexkuz/redux-devtools-inspector/commit/9dfaaabcfba7913fd15ee6ee43627e0c
eb1d5c7b
This commit is contained in:
Mihail Diordiev 2018-12-22 02:10:49 +02:00 committed by GitHub
parent 4187bc1797
commit 89880265a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 3718 additions and 51 deletions

View File

@ -0,0 +1,15 @@
{
"presets": ["es2015", "stage-0", "react"],
"plugins": ["transform-runtime"],
"env": {
"development": {
"plugins": [["react-transform", {
"transforms": [{
"transform": "react-transform-hmr",
"imports": ["react"],
"locals": ["module"]
}]
}]]
}
}
}

View File

@ -0,0 +1,57 @@
{
"parser": "babel-eslint",
"rules": {
"no-undef": ["error"],
"no-trailing-spaces": ["warn"],
"space-before-blocks": ["warn", "always"],
"no-unused-expressions": ["off"],
"no-underscore-dangle": ["off"],
"quote-props": ["warn", "as-needed"],
"no-multi-spaces": ["off"],
"no-unused-vars": ["warn"],
"no-loop-func": ["off"],
"key-spacing": ["off"],
"max-len": ["warn", 100],
"strict": ["off"],
"eol-last": ["warn"],
"no-console": ["warn"],
"indent": ["warn", 2],
"quotes": ["warn", "single", "avoid-escape"],
"curly": ["off"],
"jsx-quotes": ["warn", "prefer-single"],
"react/jsx-boolean-value": "warn",
"react/jsx-no-undef": "error",
"react/jsx-uses-react": "warn",
"react/jsx-uses-vars": "warn",
"react/no-did-mount-set-state": "warn",
"react/no-did-update-set-state": "warn",
"react/no-multi-comp": "off",
"react/no-unknown-property": "error",
"react/react-in-jsx-scope": "error",
"react/self-closing-comp": "warn",
"react/jsx-wrap-multilines": "warn",
"generator-star-spacing": "off",
"new-cap": "off",
"object-curly-spacing": "off",
"object-shorthand": "off",
"babel/generator-star-spacing": "warn",
"babel/new-cap": "warn",
"babel/object-curly-spacing": ["warn", "always"],
"babel/object-shorthand": "warn"
},
"plugins": [
"react",
"babel"
],
"settings": {
"ecmascript": 6,
"jsx": true
},
"env": {
"browser": true,
"node": true
}
}

View File

@ -0,0 +1,8 @@
static
src
demo
.*
webpack.config.js
index.html
*.gif
*.png

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2015 Alexander Kuznetsov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,68 @@
# redux-devtools-inspector
[![npm version](https://badge.fury.io/js/redux-devtools-inspector.svg)](https://badge.fury.io/js/redux-devtools-inspector)
A state monitor for [Redux DevTools](https://github.com/reduxjs/redux-devtools) that provides a convenient way to inspect "real world" app states that could be complicated and deeply nested. Created by [@alexkuz](https://github.com/alexkuz) and merged from [`alexkuz/redux-devtools-inspector`](https://github.com/romseguy/map2tree) into [`reduxjs/redux-devtools` monorepo](https://github.com/reduxjs/redux-devtools).
![](https://raw.githubusercontent.com/alexkuz/redux-devtools-inspector/master/demo.gif)
### Installation
```
npm install --save-dev redux-devtools-inspector
```
### Usage
You can use `Inspector` as the only monitor in your app:
##### `containers/DevTools.js`
```js
import React from 'react';
import { createDevTools } from 'redux-devtools';
import Inspector from 'redux-devtools-inspector';
export default createDevTools(
<Inspector />
);
```
Then you can render `<DevTools>` to any place inside app or even into a separate popup window.
Alternative, you can use it together with [`DockMonitor`](https://github.com/gaearon/redux-devtools-dock-monitor) to make it dockable.
Consult the [`DockMonitor` README](https://github.com/gaearon/redux-devtools-dock-monitor) for details of this approach.
[Read how to start using Redux DevTools.](https://github.com/gaearon/redux-devtools)
### Features
The inspector displays a list of actions and a preview panel which shows the state after the selected action and a diff with the previous state. If no actions are selected, the last state is shown.
You may pin a certain part of the state to only track its changes.
### Props
Name | Type | Description
------------------ | ---------------- | -------------
`theme` | Object or string | Contains either [base16](https://github.com/chriskempson/base16) theme name or object, that can be `base16` colors map or object containing classnames or styles.
`invertTheme` | Boolean | Inverts theme color luminance, making light theme out of dark theme and vice versa.
`supportImmutable` | Boolean | Better `Immutable` rendering in `Diff` (can affect performance if state has huge objects/arrays). `false` by default.
`tabs` | Array or function | Overrides list of tabs (see below)
`diffObjectHash` | Function | Optional callback for better array handling in diffs (see [jsondiffpatch docs](https://github.com/benjamine/jsondiffpatch/blob/master/docs/arrays.md))
`diffPropertyFilter` | Function | Optional callback for ignoring particular props in diff (see [jsondiffpatch docs](https://github.com/benjamine/jsondiffpatch#options))
If `tabs` is a function, it receives a list of default tabs and should return updated list, for example:
```
defaultTabs => [...defaultTabs, { name: 'My Tab', component: MyTab }]
```
If `tabs` is an array, only provided tabs are rendered.
`component` is provided with `action` and other props, see [`ActionPreview.jsx`](src/ActionPreview.jsx#L42) for reference.
Usage example: [`redux-devtools-test-generator`](https://github.com/zalmoxisus/redux-devtools-test-generator#containersdevtoolsjs).
### License
MIT

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -0,0 +1,3 @@
{
"import": true
}

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title><%= htmlWebpackPlugin.options.package.name %></title>
<meta name="description" content="<%= htmlWebpackPlugin.options.package.description %>">
<link href="http://fonts.googleapis.com/css?family=Noto+Sans|Roboto:400,300,500" rel="stylesheet" type="text/css">
<link href="//maxcdn.bootstrapcdn.com/bootswatch/3.3.5/paper/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<a href="<%= htmlWebpackPlugin.options.package.repository.url %>"><img style="z-index: 999999999; position: fixed; top: 0; left: 0; border: 0;" src="https://camo.githubusercontent.com/121cd7cbdc3e4855075ea8b558508b91ac463ac2/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f6c6566745f677265656e5f3030373230302e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_left_green_007200.png"></a>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,248 @@
import React from 'react';
import PageHeader from 'react-bootstrap/lib/PageHeader';
import { connect } from 'react-redux';
import pkg from '../../../package.json';
import Button from 'react-bootstrap/lib/Button';
import FormGroup from 'react-bootstrap/lib/FormGroup';
import FormControl from 'react-bootstrap/lib/FormControl';
import ControlLabel from 'react-bootstrap/lib/ControlLabel';
import Form from 'react-bootstrap/lib/Form';
import Col from 'react-bootstrap/lib/Col';
import InputGroup from 'react-bootstrap/lib/InputGroup';
import Combobox from 'react-input-enhancements/lib/Combobox';
import * as base16 from 'base16';
import * as inspectorThemes from '../../../src/themes';
import getOptions from './getOptions';
import { push as pushRoute } from 'react-router-redux';
const styles = {
wrapper: {
height: '100vh',
width: '80%',
margin: '0 auto',
paddingTop: '1px'
},
header: {
},
content: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '50%'
},
buttons: {
display: 'flex',
width: '40rem',
justifyContent: 'center',
flexWrap: 'wrap'
},
muted: {
color: '#CCCCCC'
},
button: {
margin: '0.5rem'
},
links: {
textAlign: 'center'
},
link: {
margin: '0 0.5rem',
cursor: 'pointer',
display: 'block'
},
input: {
display: 'inline-block',
textAlign: 'left',
width: '30rem'
}
};
const themeOptions = [
...Object.keys(inspectorThemes)
.map(value => ({ value, label: inspectorThemes[value].scheme })),
null,
...Object.keys(base16)
.map(value => ({ value, label: base16[value].scheme }))
.filter(opt => opt.label)
];
const ROOT = process.env.NODE_ENV === 'production' ? '/redux-devtools-inspector/' : '/';
function buildUrl(options) {
return `${ROOT}?` + [
options.useExtension ? 'ext' : '',
options.supportImmutable ? 'immutable' : '',
options.theme ? 'theme=' + options.theme : '',
options.dark ? 'dark' : ''
].filter(s => s).join('&');
}
class DemoApp extends React.Component {
render() {
const options = getOptions();
return (
<div style={styles.wrapper}>
<PageHeader style={styles.header}>
{pkg.name || <span style={styles.muted}>Package Name</span>}
</PageHeader>
<h5>{pkg.description || <span style={styles.muted}>Package Description</span>}</h5>
<div style={styles.links}>
<div style={styles.input}>
<Form horizontal>
<FormGroup>
<Col componentClass={ControlLabel} sm={3}>
Theme:
</Col>
<Col sm={9}>
<InputGroup>
<Combobox options={themeOptions}
value={options.theme}
onSelect={value => this.setTheme(options, value)}
optionFilters={[]}>
{props => <FormControl {...props} type='text' />}
</Combobox>
<InputGroup.Addon>
<a onClick={this.toggleTheme}
style={styles.link}>
{options.dark ? 'Light theme' : 'Dark theme'}
</a>
</InputGroup.Addon>
</InputGroup>
</Col>
</FormGroup>
</Form>
</div>
</div>
<div style={styles.content}>
<div style={styles.buttons}>
<Button onClick={this.props.increment} style={styles.button}>
Increment
</Button>
<Button onClick={this.props.push} style={styles.button}>
Push
</Button>
<Button onClick={this.props.pop} style={styles.button}>
Pop
</Button>
<Button onClick={this.props.replace} style={styles.button}>
Replace
</Button>
<Button onClick={this.props.changeNested} style={styles.button}>
Change Nested
</Button>
<Button onClick={this.props.pushHugeArray} style={styles.button}>
Push Huge Array
</Button>
<Button onClick={this.props.addHugeObect} style={styles.button}>
Add Huge Object
</Button>
<Button onClick={this.props.addIterator} style={styles.button}>
Add Iterator
</Button>
<Button onClick={this.props.addRecursive} style={styles.button}>
Add Recursive
</Button>
<Button onClick={this.props.addNativeMap} style={styles.button}>
Add Native Map
</Button>
<Button onClick={this.props.addImmutableMap} style={styles.button}>
Add Immutable Map
</Button>
<Button onClick={this.props.changeImmutableNested} style={styles.button}>
Change Immutable Nested
</Button>
<Button onClick={this.props.hugePayload} style={styles.button}>
Huge Payload
</Button>
<Button onClick={this.props.addFunction} style={styles.button}>
Add Function
</Button>
<Button onClick={this.props.addSymbol} style={styles.button}>
Add Symbol
</Button>
<Button onClick={this.toggleTimeoutUpdate} style={styles.button}>
Timeout Update {this.props.timeoutUpdateEnabled ? 'On' : 'Off'}
</Button>
<Button onClick={this.props.shuffleArray} style={styles.button}>
Shuffle Array
</Button>
</div>
</div>
<div style={styles.links}>
<a onClick={this.toggleExtension}
style={styles.link}>
{(options.useExtension ? 'Disable' : 'Enable') + ' Chrome Extension'}
</a>
<a onClick={this.toggleImmutableSupport}
style={styles.link}>
{(options.supportImmutable ? 'Disable' : 'Enable') + ' Full Immutable Support'}
</a>
</div>
</div>
);
}
toggleExtension = () => {
const options = getOptions();
this.props.pushRoute(buildUrl({ ...options, useExtension: !options.useExtension }));
};
toggleImmutableSupport = () => {
const options = getOptions();
this.props.pushRoute(buildUrl({ ...options, supportImmutable: !options.supportImmutable }));
};
toggleTheme = () => {
const options = getOptions();
this.props.pushRoute(buildUrl({ ...options, dark: !options.dark }));
};
setTheme = (options, theme) => {
this.props.pushRoute(buildUrl({ ...options, theme }));
};
toggleTimeoutUpdate = () => {
const enabled = !this.props.timeoutUpdateEnabled;
this.props.toggleTimeoutUpdate(enabled);
if (enabled) {
this.timeout = setInterval(this.props.timeoutUpdate, 1000);
} else {
clearTimeout(this.timeout);
}
}
}
export default connect(
state => state,
{
toggleTimeoutUpdate: timeoutUpdateEnabled => ({
type: 'TOGGLE_TIMEOUT_UPDATE', timeoutUpdateEnabled
}),
timeoutUpdate: () => ({ type: 'TIMEOUT_UPDATE' }),
increment: () => ({ type: 'INCREMENT' }),
push: () => ({ type: 'PUSH' }),
pop: () => ({ type: 'POP' }),
replace: () => ({ type: 'REPLACE' }),
changeNested: () => ({ type: 'CHANGE_NESTED' }),
pushHugeArray: () => ({ type: 'PUSH_HUGE_ARRAY' }),
addIterator: () => ({ type: 'ADD_ITERATOR' }),
addHugeObect: () => ({ type: 'ADD_HUGE_OBJECT' }),
addRecursive: () => ({ type: 'ADD_RECURSIVE' }),
addNativeMap: () => ({ type: 'ADD_NATIVE_MAP' }),
addImmutableMap: () => ({ type: 'ADD_IMMUTABLE_MAP' }),
changeImmutableNested: () => ({ type: 'CHANGE_IMMUTABLE_NESTED' }),
hugePayload: () => ({
type: 'HUGE_PAYLOAD',
payload: Array.from({ length: 10000 }).map((_, i) => i)
}),
addFunction: () => ({ type: 'ADD_FUNCTION' }),
addSymbol: () => ({ type: 'ADD_SYMBOL' }),
shuffleArray: () => ({ type: 'SHUFFLE_ARRAY' }),
pushRoute
}
)(DemoApp);

View File

@ -0,0 +1,11 @@
export default function getOptions() {
return {
useExtension: window.location.search.indexOf('ext') !== -1,
supportImmutable: window.location.search.indexOf('immutable') !== -1,
theme: do {
const match = window.location.search.match(/theme=([^&]+)/);
match ? match[1] : 'inspector'
},
dark: window.location.search.indexOf('dark') !== -1
};
}

View File

@ -0,0 +1,100 @@
import 'babel-polyfill';
import React from 'react';
import { render } from 'react-dom';
import DemoApp from './DemoApp';
import { Provider } from 'react-redux';
import reducers from './reducers';
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import createLogger from 'redux-logger';
import { Router, Route, browserHistory } from 'react-router';
import { syncHistoryWithStore, routerReducer, routerMiddleware } from 'react-router-redux';
import { createDevTools, persistState } from 'redux-devtools';
import DevtoolsInspector from '../../../src/DevtoolsInspector';
import DockMonitor from 'redux-devtools-dock-monitor';
import getOptions from './getOptions';
function getDebugSessionKey() {
const matches = window.location.href.match(/[?&]debug_session=([^&#]+)\b/);
return (matches && matches.length > 0)? matches[1] : null;
}
const CustomComponent = () =>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
minHeight: '20rem'
}}>
<div>Custom Tab Content</div>
</div>;
const getDevTools = options =>
createDevTools(
<DockMonitor defaultIsVisible
toggleVisibilityKey='ctrl-h'
changePositionKey='ctrl-q'
changeMonitorKey='ctrl-m'>
<DevtoolsInspector theme={options.theme}
shouldPersistState
invertTheme={!options.dark}
supportImmutable={options.supportImmutable}
tabs={defaultTabs => [{
name: 'Custom Tab',
component: CustomComponent
}, ...defaultTabs]} />
</DockMonitor>
);
const ROOT = process.env.NODE_ENV === 'production' ? '/redux-devtools-inspector/' : '/';
let DevTools = getDevTools(getOptions());
const reduxRouterMiddleware = routerMiddleware(browserHistory);
const enhancer = compose(
applyMiddleware(createLogger(), reduxRouterMiddleware),
(...args) => {
const useDevtoolsExtension = !!window.__REDUX_DEVTOOLS_EXTENSION__ && getOptions().useExtension;
const instrument = useDevtoolsExtension ?
window.__REDUX_DEVTOOLS_EXTENSION__() : DevTools.instrument();
return instrument(...args);
},
persistState(getDebugSessionKey())
);
const store = createStore(combineReducers({
...reducers,
routing: routerReducer
}), {}, enhancer);
const history = syncHistoryWithStore(browserHistory, store);
const handleRouterUpdate = () => {
renderApp(getOptions());
};
const router = (
<Router history={history} onUpdate={handleRouterUpdate}>
<Route path={ROOT}
component={DemoApp} />
</Router>
);
const renderApp = options => {
DevTools = getDevTools(options);
const useDevtoolsExtension = !!window.__REDUX_DEVTOOLS_EXTENSION__ && options.useExtension;
return render(
<Provider store={store}>
<div>
{router}
{!useDevtoolsExtension && <DevTools />}
</div>
</Provider>,
document.getElementById('root')
);
}
renderApp(getOptions());

View File

@ -0,0 +1,123 @@
import Immutable from 'immutable';
import shuffle from 'lodash.shuffle';
const NESTED = {
long: {
nested: [{
path: {
to: {
a: 'key'
}
}
}]
}
};
const IMMUTABLE_NESTED = Immutable.fromJS(NESTED);
/* eslint-disable babel/new-cap */
const IMMUTABLE_MAP = Immutable.Map({
map: Immutable.Map({ a:1, b: 2, c: 3 }),
list: Immutable.List(['a', 'b', 'c']),
set: Immutable.Set(['a', 'b', 'c']),
stack: Immutable.Stack(['a', 'b', 'c']),
seq: Immutable.Seq.of(1, 2, 3, 4, 5, 6, 7, 8)
});
const NATIVE_MAP = new window.Map([
['map', new window.Map([
[{ first: true }, 1],
['second', 2]
])],
['weakMap', new window.WeakMap([
[{ first: true }, 1],
[{ second: 1 }, 2]
])],
['set', new window.Set([
{ first: true },
'second'
])],
['weakSet', new window.WeakSet([
{ first: true },
{ second: 1 }
])]
]);
/* eslint-enable babel/new-cap */
const HUGE_ARRAY = Array.from({ length: 5000 })
.map((_, key) => ({ str: 'key ' + key }));
const HUGE_OBJECT = Array.from({ length: 5000 })
.reduce((o, _, key) => (o['key ' + key] = 'item ' + key, o), {});
const FUNC = function (a, b, c) { return a + b + c; };
const RECURSIVE = {};
RECURSIVE.obj = RECURSIVE;
function createIterator() {
const iterable = {};
iterable[window.Symbol.iterator] = function *iterator() {
for (var i = 0; i < 333; i++) {
yield 'item ' + i;
}
}
return iterable;
}
const DEFAULT_SHUFFLE_ARRAY = [0, 1, null, { id: 1 }, { id: 2 }, 'string'];
export default {
timeoutUpdateEnabled: (state=false, action) => action.type === 'TOGGLE_TIMEOUT_UPDATE' ?
action.timeoutUpdateEnabled : state,
store: (state=0, action) => action.type === 'INCREMENT' ? state + 1 : state,
undefined: (state={ val: undefined }) => state,
null: (state=null) => state,
func: (state=() => {}) => state,
array: (state=[], action) => action.type === 'PUSH' ?
[...state, Math.random()] : (
action.type === 'POP' ? state.slice(0, state.length - 1) : (
action.type === 'REPLACE' ? [Math.random(), ...state.slice(1)] : state
)
),
hugeArrays: (state=[], action) => action.type === 'PUSH_HUGE_ARRAY' ?
[ ...state, ...HUGE_ARRAY ] : state,
hugeObjects: (state=[], action) => action.type === 'ADD_HUGE_OBJECT' ?
[ ...state, HUGE_OBJECT ] : state,
iterators: (state=[], action) => action.type === 'ADD_ITERATOR' ?
[...state, createIterator()] : state,
nested: (state=NESTED, action) =>
action.type === 'CHANGE_NESTED' ? {
...state,
long: {
nested: [{
path: {
to: {
a: state.long.nested[0].path.to.a + '!'
}
}
}]
}
} : state,
recursive: (state=[], action) => action.type === 'ADD_RECURSIVE' ?
[...state, { ...RECURSIVE }] : state,
immutables: (state=[], action) => action.type === 'ADD_IMMUTABLE_MAP' ?
[...state, IMMUTABLE_MAP] : state,
maps: (state=[], action) => action.type === 'ADD_NATIVE_MAP' ?
[...state, NATIVE_MAP] : state,
immutableNested: (state=IMMUTABLE_NESTED, action) => action.type === 'CHANGE_IMMUTABLE_NESTED' ?
state.updateIn(
['long', 'nested', 0, 'path', 'to', 'a'],
str => str + '!'
) : state,
addFunction: (state=null, action) => action.type === 'ADD_FUNCTION' ?
{ f: FUNC } : state,
addSymbol: (state=null, action) => action.type === 'ADD_SYMBOL' ?
{ s: window.Symbol('symbol'), error: new Error('TEST') } : state,
shuffleArray: (state=DEFAULT_SHUFFLE_ARRAY, action) =>
action.type === 'SHUFFLE_ARRAY' ?
shuffle(state) : state
};

View File

@ -0,0 +1,92 @@
{
"name": "redux-devtools-inspector",
"version": "0.11.0",
"description": "Redux DevTools Diff Monitor",
"scripts": {
"build:lib": "NODE_ENV=production babel src --out-dir lib",
"build:demo": "NODE_ENV=production webpack -p",
"stats": "webpack --profile --json > stats.json",
"start": "webpack-dev-server",
"lint": "eslint --ext .jsx,.js --max-warnings 0 src",
"preversion": "npm run lint",
"version": "npm run build:demo && git add -A .",
"postversion": "git push",
"prepublish": "npm run build:lib",
"gh": "git subtree push --prefix demo/dist origin gh-pages"
},
"main": "lib/index.js",
"repository": {
"url": "https://github.com/reduxjs/redux-devtools"
},
"devDependencies": {
"babel": "^6.3.26",
"babel-cli": "^6.4.5",
"babel-core": "^6.4.5",
"babel-eslint": "^7.1.0",
"babel-loader": "^6.2.2",
"babel-plugin-react-transform": "^2.0.0",
"babel-plugin-transform-runtime": "^6.4.3",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"babel-preset-stage-0": "^6.3.13",
"base16": "^1.0.0",
"chokidar": "^1.6.1",
"clean-webpack-plugin": "^0.1.8",
"eslint": "^4.0.0",
"eslint-loader": "^1.2.1",
"eslint-plugin-babel": "^4.0.0",
"eslint-plugin-react": "^6.6.0",
"export-files-webpack-plugin": "0.0.1",
"html-webpack-plugin": "^2.8.1",
"imports-loader": "^0.6.5",
"json-loader": "^0.5.4",
"lodash.shuffle": "^4.2.0",
"nyan-progress-webpack-plugin": "^1.1.4",
"pre-commit": "^1.1.3",
"raw-loader": "^0.5.1",
"react": "^16.4.2",
"react-bootstrap": "^0.30.6",
"react-dom": "^16.4.2",
"react-input-enhancements": "^0.7.5",
"react-redux": "^6.0.0",
"react-router": "^3.0.0",
"react-router-redux": "^4.0.2",
"react-transform-hmr": "^1.0.2",
"redux": "^4.0.0",
"redux-devtools": "^3.1.0",
"redux-devtools-dock-monitor": "^1.0.1",
"redux-logger": "^2.5.2",
"webpack": "^1.12.13",
"webpack-dev-server": "^1.14.1"
},
"peerDependencies": {
"react": ">=15.0.0",
"react-dom": ">=15.0.0"
},
"author": "Alexander <alexkuz@gmail.com> (http://kuzya.org/)",
"contributors": [
"Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)"
],
"license": "MIT",
"dependencies": {
"babel-runtime": "^6.3.19",
"dateformat": "^1.0.12",
"hex-rgba": "^1.0.0",
"immutable": "^3.7.6",
"javascript-stringify": "^1.1.0",
"jsondiffpatch": "^0.2.4",
"jss": "^6.0.0",
"jss-nested": "^3.0.0",
"jss-vendor-prefixer": "^4.0.0",
"lodash.debounce": "^4.0.3",
"prop-types": "^15.6.2",
"react-base16-styling": "^0.4.1",
"react-dragula": "^1.1.17",
"react-json-tree": "^0.11.1",
"react-pure-render": "^1.0.2",
"redux-devtools-themes": "^1.0.0"
},
"pre-commit": [
"lint"
]
}

View File

@ -0,0 +1,3 @@
{
"import": true
}

View File

@ -0,0 +1,124 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import dragula from 'react-dragula';
import ActionListRow from './ActionListRow';
import ActionListHeader from './ActionListHeader';
import shouldPureComponentUpdate from 'react-pure-render/function';
function getTimestamps(actions, actionIds, actionId) {
const idx = actionIds.indexOf(actionId);
const prevActionId = actionIds[idx - 1];
return {
current: actions[actionId].timestamp,
previous: idx ? actions[prevActionId].timestamp : 0
};
}
export default class ActionList extends Component {
shouldComponentUpdate = shouldPureComponentUpdate;
componentWillReceiveProps(nextProps) {
const node = this.node;
if (!node) {
this.scrollDown = true;
} else if (this.props.lastActionId !== nextProps.lastActionId) {
const { scrollTop, offsetHeight, scrollHeight } = node;
this.scrollDown = Math.abs(scrollHeight - (scrollTop + offsetHeight)) < 50;
} else {
this.scrollDown = false;
}
}
componentDidMount() {
this.scrollDown = true;
this.scrollToBottom();
if (!this.props.draggableActions) return;
const container = ReactDOM.findDOMNode(this.refs.rows);
this.drake = dragula([container], {
copy: false,
copySortSource: false,
mirrorContainer: container,
accepts: (el, target, source, sibling) => (
!sibling || parseInt(sibling.getAttribute('data-id'))
),
moves: (el, source, handle) => (
parseInt(el.getAttribute('data-id')) &&
handle.className.indexOf('selectorButton') !== 0
),
}).on('drop', (el, target, source, sibling) => {
let beforeActionId = this.props.actionIds.length;
if (sibling && sibling.className.indexOf('gu-mirror') === -1) {
beforeActionId = parseInt(sibling.getAttribute('data-id'));
}
const actionId = parseInt(el.getAttribute('data-id'));
this.props.onReorderAction(actionId, beforeActionId)
});
}
componentWillUnmount() {
if (this.drake) this.drake.destroy();
}
componentDidUpdate() {
this.scrollToBottom();
}
scrollToBottom() {
if (this.scrollDown && this.node) {
this.node.scrollTop = this.node.scrollHeight;
}
}
getRef = node => {
this.node = node;
}
render() {
const { styling, actions, actionIds, isWideLayout, onToggleAction, skippedActionIds,
selectedActionId, startActionId, onSelect, onSearch, searchValue, currentActionId,
hideMainButtons, hideActionButtons, onCommit, onSweep, onJumpToState } = this.props;
const lowerSearchValue = searchValue && searchValue.toLowerCase();
const filteredActionIds = searchValue ? actionIds.filter(
id => actions[id].action.type.toLowerCase().indexOf(lowerSearchValue) !== -1
) : actionIds;
return (
<div key='actionList'
{...styling(['actionList', isWideLayout && 'actionListWide'], isWideLayout)}>
<ActionListHeader styling={styling}
onSearch={onSearch}
onCommit={onCommit}
onSweep={onSweep}
hideMainButtons={hideMainButtons}
hasSkippedActions={skippedActionIds.length > 0}
hasStagedActions={actionIds.length > 1} />
<div {...styling('actionListRows')} ref={this.getRef}>
{filteredActionIds.map(actionId =>
(<ActionListRow key={actionId}
styling={styling}
actionId={actionId}
isInitAction={!actionId}
isSelected={
startActionId !== null &&
actionId >= startActionId && actionId <= selectedActionId ||
actionId === selectedActionId
}
isInFuture={
actionIds.indexOf(actionId) > actionIds.indexOf(currentActionId)
}
onSelect={(e) => onSelect(e, actionId)}
timestamps={getTimestamps(actions, actionIds, actionId)}
action={actions[actionId].action}
onToggleClick={() => onToggleAction(actionId)}
onJumpClick={() => onJumpToState(actionId)}
onCommitClick={() => onCommit(actionId)}
hideActionButtons={hideActionButtons}
isSkipped={skippedActionIds.indexOf(actionId) !== -1} />)
)}
</div>
</div>
);
}
}

View File

@ -0,0 +1,43 @@
import React from 'react';
import RightSlider from './RightSlider';
const getActiveButtons = (hasSkippedActions) => [
hasSkippedActions && 'Sweep',
'Commit'
].filter(a => a);
const ActionListHeader =
({
styling, onSearch, hasSkippedActions, hasStagedActions, onCommit, onSweep, hideMainButtons
}) =>
(<div {...styling('actionListHeader')}>
<input
{...styling('actionListHeaderSearch')}
onChange={e => onSearch(e.target.value)}
placeholder='filter...'
/>
{!hideMainButtons &&
<div {...styling('actionListHeaderWrapper')}>
<RightSlider shown={hasStagedActions} styling={styling}>
<div {...styling('actionListHeaderSelector')}>
{getActiveButtons(hasSkippedActions).map(btn =>
(<div
key={btn}
onClick={() => ({
Commit: onCommit,
Sweep: onSweep
})[btn]()}
{...styling([
'selectorButton',
'selectorButtonSmall'], false, true)}
>
{btn}
</div>)
)}
</div>
</RightSlider>
</div>
}
</div>);
export default ActionListHeader;

View File

@ -0,0 +1,129 @@
import React, { Component } from 'react';
import { PropTypes } from 'prop-types';
import shouldPureComponentUpdate from 'react-pure-render/function';
import dateformat from 'dateformat';
import debounce from 'lodash.debounce';
import RightSlider from './RightSlider';
const BUTTON_SKIP = 'Skip';
const BUTTON_JUMP = 'Jump';
export default class ActionListRow extends Component {
state = { hover: false };
static propTypes = {
styling: PropTypes.func.isRequired,
isSelected: PropTypes.bool.isRequired,
action: PropTypes.object.isRequired,
isInFuture: PropTypes.bool.isRequired,
isInitAction: PropTypes.bool.isRequired,
onSelect: PropTypes.func.isRequired,
timestamps: PropTypes.shape({
current: PropTypes.number.isRequired,
previous: PropTypes.number.isRequired
}).isRequired,
isSkipped: PropTypes.bool.isRequired
};
shouldComponentUpdate = shouldPureComponentUpdate
render() {
const { styling, isSelected, action, actionId, isInitAction, onSelect,
timestamps, isSkipped, isInFuture, hideActionButtons } = this.props;
const { hover } = this.state;
const timeDelta = timestamps.current - timestamps.previous;
const showButtons = hover && !isInitAction || isSkipped;
const isButtonSelected = btn =>
btn === BUTTON_SKIP && isSkipped;
let actionType = action.type;
if (typeof actionType === 'undefined') actionType = '<UNDEFINED>';
else if (actionType === null) actionType = '<NULL>';
else actionType = actionType.toString() || '<EMPTY>';
return (
<div onClick={onSelect}
onMouseEnter={!hideActionButtons && this.handleMouseEnter}
onMouseLeave={!hideActionButtons && this.handleMouseLeave}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseEnter}
data-id={actionId}
{...styling([
'actionListItem',
isSelected && 'actionListItemSelected',
isSkipped && 'actionListItemSkipped',
isInFuture && 'actionListFromFuture'
], isSelected, action)}>
<div {...styling(['actionListItemName', isSkipped && 'actionListItemNameSkipped'])}>
{actionType}
</div>
{hideActionButtons ?
<RightSlider styling={styling} shown>
<div {...styling('actionListItemTime')}>
{timeDelta === 0 ? '+00:00:00' :
dateformat(timeDelta, timestamps.previous ? '+MM:ss.L' : 'h:MM:ss.L')}
</div>
</RightSlider>
:
<div {...styling('actionListItemButtons')}>
<RightSlider styling={styling} shown={!showButtons} rotate>
<div {...styling('actionListItemTime')}>
{timeDelta === 0 ? '+00:00:00' :
dateformat(timeDelta, timestamps.previous ? '+MM:ss.L' : 'h:MM:ss.L')}
</div>
</RightSlider>
<RightSlider styling={styling} shown={showButtons} rotate>
<div {...styling('actionListItemSelector')}>
{[BUTTON_JUMP, BUTTON_SKIP].map(btn => (!isInitAction || btn !== BUTTON_SKIP) &&
<div key={btn}
onClick={this.handleButtonClick.bind(this, btn)}
{...styling([
'selectorButton',
isButtonSelected(btn) && 'selectorButtonSelected',
'selectorButtonSmall'], isButtonSelected(btn), true)}>
{btn}
</div>
)}
</div>
</RightSlider>
</div>
}
</div>
);
}
handleButtonClick(btn, e) {
e.stopPropagation();
switch(btn) {
case BUTTON_SKIP:
this.props.onToggleClick();
break;
case BUTTON_JUMP:
this.props.onJumpClick();
break;
}
}
handleMouseEnter = e => {
if (this.hover) return;
this.handleMouseLeave.cancel();
this.handleMouseEnterDebounced(e.buttons);
}
handleMouseEnterDebounced = debounce((buttons) => {
if (buttons) return;
this.setState({ hover: true });
}, 150)
handleMouseLeave = debounce(() => {
this.handleMouseEnterDebounced.cancel();
if (this.state.hover) this.setState({ hover: false });
}, 100)
handleMouseDown = e => {
if (e.target.className.indexOf('selectorButton') === 0) return;
this.handleMouseLeave();
}
}

View File

@ -0,0 +1,97 @@
import React, { Component } from 'react';
import { DEFAULT_STATE } from './redux';
import ActionPreviewHeader from './ActionPreviewHeader';
import DiffTab from './tabs/DiffTab';
import StateTab from './tabs/StateTab';
import ActionTab from './tabs/ActionTab';
const DEFAULT_TABS = [{
name: 'Action',
component: ActionTab
}, {
name: 'Diff',
component: DiffTab
}, {
name: 'State',
component: StateTab
}]
class ActionPreview extends Component {
static defaultProps = {
tabName: DEFAULT_STATE.tabName
}
render() {
const {
styling, delta, error, nextState, onInspectPath, inspectedPath, tabName,
isWideLayout, onSelectTab, action, actions, selectedActionId, startActionId,
computedStates, base16Theme, invertTheme, tabs, dataTypeKey, monitorState, updateMonitorState
} = this.props;
const renderedTabs = (typeof tabs === 'function') ?
tabs(DEFAULT_TABS) :
(tabs ? tabs : DEFAULT_TABS);
const { component: TabComponent } = (
renderedTabs.find(tab => tab.name === tabName)
|| renderedTabs.find(tab => tab.name === DEFAULT_STATE.tabName)
);
return (
<div key='actionPreview' {...styling('actionPreview')}>
<ActionPreviewHeader
tabs={renderedTabs}
{...{ styling, inspectedPath, onInspectPath, tabName, onSelectTab }}
/>
{!error &&
<div key='actionPreviewContent' {...styling('actionPreviewContent')}>
<TabComponent
labelRenderer={this.labelRenderer}
{...{
styling,
computedStates,
actions,
selectedActionId,
startActionId,
base16Theme,
invertTheme,
isWideLayout,
dataTypeKey,
delta,
action,
nextState,
monitorState,
updateMonitorState
}}
/>
</div>
}
{error &&
<div {...styling('stateError')}>{error}</div>
}
</div>
);
}
labelRenderer = ([key, ...rest], nodeType, expanded) => {
const { styling, onInspectPath, inspectedPath } = this.props;
return (
<span>
<span {...styling('treeItemKey')}>
{key}
</span>
<span {...styling('treeItemPin')}
onClick={() => onInspectPath([
...inspectedPath.slice(0, inspectedPath.length - 1),
...[key, ...rest].reverse()
])}>
{'(pin)'}
</span>
{!expanded && ': '}
</span>
);
}
}
export default ActionPreview;

View File

@ -0,0 +1,40 @@
import React from 'react';
const ActionPreviewHeader =
({ styling, inspectedPath, onInspectPath, tabName, onSelectTab, tabs }) =>
(<div key='previewHeader' {...styling('previewHeader')}>
<div {...styling('tabSelector')}>
{tabs.map(tab =>
(<div onClick={() => onSelectTab(tab.name)}
key={tab.name}
{...styling([
'selectorButton',
tab.name === tabName && 'selectorButtonSelected'
], tab.name === tabName)}>
{tab.name}
</div>)
)}
</div>
<div {...styling('inspectedPath')}>
{inspectedPath.length ?
<span {...styling('inspectedPathKey')}>
<a onClick={() => onInspectPath([])}
{...styling('inspectedPathKeyLink')}>
{tabName}
</a>
</span> : tabName
}
{inspectedPath.map((key, idx) =>
idx === inspectedPath.length - 1 ? <span key={key}>{key}</span> :
<span key={key}
{...styling('inspectedPathKey')}>
<a onClick={() => onInspectPath(inspectedPath.slice(0, idx + 1))}
{...styling('inspectedPathKeyLink')}>
{key}
</a>
</span>
)}
</div>
</div>);
export default ActionPreviewHeader;

View File

@ -0,0 +1,270 @@
import React, { Component } from 'react';
import { PropTypes } from 'prop-types';
import { createStylingFromTheme, base16Themes } from './utils/createStylingFromTheme';
import shouldPureComponentUpdate from 'react-pure-render/function';
import ActionList from './ActionList';
import ActionPreview from './ActionPreview';
import getInspectedState from './utils/getInspectedState';
import createDiffPatcher from './createDiffPatcher';
import { getBase16Theme } from 'react-base16-styling';
import { reducer, updateMonitorState } from './redux';
import { ActionCreators } from 'redux-devtools';
const { commit, sweep, toggleAction, jumpToAction, jumpToState, reorderAction } = ActionCreators;
function getLastActionId(props) {
return props.stagedActionIds[props.stagedActionIds.length - 1];
}
function getCurrentActionId(props, monitorState) {
return monitorState.selectedActionId === null ?
props.stagedActionIds[props.currentStateIndex] : monitorState.selectedActionId;
}
function getFromState(actionIndex, stagedActionIds, computedStates, monitorState) {
const { startActionId } = monitorState;
if (startActionId === null) {
return actionIndex > 0 ? computedStates[actionIndex - 1] : null;
}
let fromStateIdx = stagedActionIds.indexOf(startActionId - 1);
if (fromStateIdx === -1) fromStateIdx = 0;
return computedStates[fromStateIdx];
}
function createIntermediateState(props, monitorState) {
const { supportImmutable, computedStates, stagedActionIds,
actionsById: actions, diffObjectHash, diffPropertyFilter } = props;
const { inspectedStatePath, inspectedActionPath } = monitorState;
const currentActionId = getCurrentActionId(props, monitorState);
const currentAction = actions[currentActionId] && actions[currentActionId].action;
const actionIndex = stagedActionIds.indexOf(currentActionId);
const fromState = getFromState(actionIndex, stagedActionIds, computedStates, monitorState);
const toState = computedStates[actionIndex];
const error = toState && toState.error;
const fromInspectedState = !error && fromState &&
getInspectedState(fromState.state, inspectedStatePath, supportImmutable);
const toInspectedState =
!error && toState && getInspectedState(toState.state, inspectedStatePath, supportImmutable);
const delta = !error && fromState && toState &&
createDiffPatcher(diffObjectHash, diffPropertyFilter).diff(
fromInspectedState,
toInspectedState
);
return {
delta,
nextState: toState && getInspectedState(toState.state, inspectedStatePath, false),
action: getInspectedState(currentAction, inspectedActionPath, false),
error
};
}
function createThemeState(props) {
const base16Theme = getBase16Theme(props.theme, base16Themes);
const styling = createStylingFromTheme(props.theme, props.invertTheme);
return { base16Theme, styling };
}
export default class DevtoolsInspector extends Component {
constructor(props) {
super(props);
this.state = {
...createIntermediateState(props, props.monitorState),
isWideLayout: false,
themeState: createThemeState(props)
};
}
static propTypes = {
dispatch: PropTypes.func,
computedStates: PropTypes.array,
stagedActionIds: PropTypes.array,
actionsById: PropTypes.object,
currentStateIndex: PropTypes.number,
monitorState: PropTypes.shape({
initialScrollTop: PropTypes.number
}),
preserveScrollTop: PropTypes.bool,
draggableActions: PropTypes.bool,
stagedActions: PropTypes.array,
select: PropTypes.func.isRequired,
theme: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string
]),
supportImmutable: PropTypes.bool,
diffObjectHash: PropTypes.func,
diffPropertyFilter: PropTypes.func,
hideMainButtons: PropTypes.bool,
hideActionButtons: PropTypes.bool
};
static update = reducer;
static defaultProps = {
select: (state) => state,
supportImmutable: false,
draggableActions: true,
theme: 'inspector',
invertTheme: true
};
shouldComponentUpdate = shouldPureComponentUpdate;
componentDidMount() {
this.updateSizeMode();
this.updateSizeTimeout = setInterval(this.updateSizeMode.bind(this), 150);
}
componentWillUnmount() {
clearTimeout(this.updateSizeTimeout);
}
updateMonitorState = monitorState => {
this.props.dispatch(updateMonitorState(monitorState));
};
updateSizeMode() {
const isWideLayout = this.refs.inspector.offsetWidth > 500;
if (isWideLayout !== this.state.isWideLayout) {
this.setState({ isWideLayout });
}
}
componentWillReceiveProps(nextProps) {
let nextMonitorState = nextProps.monitorState;
const monitorState = this.props.monitorState;
if (
getCurrentActionId(this.props, monitorState) !==
getCurrentActionId(nextProps, nextMonitorState) ||
monitorState.startActionId !== nextMonitorState.startActionId ||
monitorState.inspectedStatePath !== nextMonitorState.inspectedStatePath ||
monitorState.inspectedActionPath !== nextMonitorState.inspectedActionPath ||
this.props.computedStates !== nextProps.computedStates ||
this.props.stagedActionIds !== nextProps.stagedActionIds
) {
this.setState(createIntermediateState(nextProps, nextMonitorState));
}
if (this.props.theme !== nextProps.theme ||
this.props.invertTheme !== nextProps.invertTheme) {
this.setState({ themeState: createThemeState(nextProps) });
}
}
render() {
const {
stagedActionIds: actionIds, actionsById: actions, computedStates, draggableActions,
tabs, invertTheme, skippedActionIds, currentStateIndex, monitorState, dataTypeKey,
hideMainButtons, hideActionButtons
} = this.props;
const { selectedActionId, startActionId, searchValue, tabName } = monitorState;
const inspectedPathType = tabName === 'Action' ? 'inspectedActionPath' : 'inspectedStatePath';
const {
themeState, isWideLayout, action, nextState, delta, error
} = this.state;
const { base16Theme, styling } = themeState;
return (
<div key='inspector'
ref='inspector'
{...styling(['inspector', isWideLayout && 'inspectorWide'], isWideLayout)}>
<ActionList {...{
actions, actionIds, isWideLayout, searchValue, selectedActionId, startActionId,
skippedActionIds, draggableActions, hideMainButtons, hideActionButtons, styling
}}
onSearch={this.handleSearch}
onSelect={this.handleSelectAction}
onToggleAction={this.handleToggleAction}
onJumpToState={this.handleJumpToState}
onCommit={this.handleCommit}
onSweep={this.handleSweep}
onReorderAction={this.handleReorderAction}
currentActionId={actionIds[currentStateIndex]}
lastActionId={getLastActionId(this.props)} />
<ActionPreview {...{
base16Theme, invertTheme, isWideLayout, tabs, tabName, delta, error, nextState,
computedStates, action, actions, selectedActionId, startActionId, dataTypeKey
}}
monitorState={this.props.monitorState}
updateMonitorState={this.updateMonitorState}
styling={styling}
onInspectPath={this.handleInspectPath.bind(this, inspectedPathType)}
inspectedPath={monitorState[inspectedPathType]}
onSelectTab={this.handleSelectTab} />
</div>
);
}
handleToggleAction = actionId => {
this.props.dispatch(toggleAction(actionId));
};
handleJumpToState = actionId => {
if (jumpToAction) {
this.props.dispatch(jumpToAction(actionId));
} else { // Fallback for redux-devtools-instrument < 1.5
const index = this.props.stagedActionIds.indexOf(actionId);
if (index !== -1) this.props.dispatch(jumpToState(index));
}
};
handleReorderAction = (actionId, beforeActionId) => {
if (reorderAction) this.props.dispatch(reorderAction(actionId, beforeActionId));
};
handleCommit = () => {
this.props.dispatch(commit());
};
handleSweep = () => {
this.props.dispatch(sweep());
};
handleSearch = val => {
this.updateMonitorState({ searchValue: val });
};
handleSelectAction = (e, actionId) => {
const { monitorState } = this.props;
let startActionId;
let selectedActionId;
if (e.shiftKey && monitorState.selectedActionId !== null) {
if (monitorState.startActionId !== null) {
if (actionId >= monitorState.startActionId) {
startActionId = Math.min(monitorState.startActionId, monitorState.selectedActionId);
selectedActionId = actionId;
} else {
selectedActionId = Math.max(monitorState.startActionId, monitorState.selectedActionId);
startActionId = actionId;
}
} else {
startActionId = Math.min(actionId, monitorState.selectedActionId);
selectedActionId = Math.max(actionId, monitorState.selectedActionId);
}
} else {
startActionId = null;
if (actionId === monitorState.selectedActionId || monitorState.startActionId !== null) {
selectedActionId = null;
} else {
selectedActionId = actionId;
}
}
this.updateMonitorState({ startActionId, selectedActionId });
};
handleInspectPath = (pathType, path) => {
this.updateMonitorState({ [pathType]: path });
};
handleSelectTab = tabName => {
this.updateMonitorState({ tabName });
};
}

View File

@ -0,0 +1,18 @@
import React from 'react';
import { PropTypes } from 'prop-types';
const RightSlider = ({ styling, shown, children, rotate }) =>
(<div {...styling([
'rightSlider',
shown && 'rightSliderShown',
rotate && 'rightSliderRotate',
rotate && shown && 'rightSliderRotateShown'
])}>
{children}
</div>);
RightSlider.propTypes = {
shown: PropTypes.bool
};
export default RightSlider;

View File

@ -0,0 +1,29 @@
import { DiffPatcher } from 'jsondiffpatch/src/diffpatcher';
const defaultObjectHash = (o, idx) =>
o === null && '$$null' ||
o && (o.id || o.id === 0) && `$$id:${JSON.stringify(o.id)}` ||
o && (o._id ||o._id === 0) && `$$_id:${JSON.stringify(o._id)}` ||
'$$index:' + idx;
const defaultPropertyFilter = (name, context) =>
typeof context.left[name] !== 'function' &&
typeof context.right[name] !== 'function';
const defaultDiffPatcher = new DiffPatcher({
arrays: { detectMove: false },
objectHash: defaultObjectHash,
propertyFilter: defaultPropertyFilter
});
export default function createDiffPatcher(objectHash, propertyFilter) {
if (!objectHash && !propertyFilter) {
return defaultDiffPatcher;
}
return new DiffPatcher({
arrays: { detectMove: false },
objectHash: objectHash || defaultObjectHash,
propertyFilter: propertyFilter || defaultPropertyFilter
});
}

View File

@ -0,0 +1 @@
export default from './DevtoolsInspector';

View File

@ -0,0 +1,26 @@
const UPDATE_MONITOR_STATE = '@@redux-devtools-inspector/UPDATE_MONITOR_STATE';
export const DEFAULT_STATE = {
selectedActionId: null,
startActionId: null,
inspectedActionPath: [],
inspectedStatePath: [],
tabName: 'Diff'
};
export function updateMonitorState(monitorState) {
return { type: UPDATE_MONITOR_STATE, monitorState };
}
function reduceUpdateState(state, action) {
return (action.type === UPDATE_MONITOR_STATE) ? {
...state,
...action.monitorState
} : state;
}
export function reducer(props, state=DEFAULT_STATE, action) {
return {
...reduceUpdateState(state, action)
};
}

View File

@ -0,0 +1,18 @@
import React from 'react';
import JSONTree from 'react-json-tree';
import getItemString from './getItemString';
import getJsonTreeTheme from './getJsonTreeTheme';
const ActionTab = ({
action, styling, base16Theme, invertTheme, labelRenderer, dataTypeKey, isWideLayout
}) =>
(<JSONTree
labelRenderer={labelRenderer}
theme={getJsonTreeTheme(base16Theme)}
data={action}
getItemString={(type, data) => getItemString(styling, type, data, dataTypeKey, isWideLayout)}
invertTheme={invertTheme}
hideRoot
/>);
export default ActionTab;

View File

@ -0,0 +1,9 @@
import React from 'react';
import JSONDiff from './JSONDiff';
const DiffTab = ({ delta, styling, base16Theme, invertTheme, labelRenderer, isWideLayout }) =>
(<JSONDiff
{...{ delta, styling, base16Theme, invertTheme, labelRenderer, isWideLayout }}
/>);
export default DiffTab;

View File

@ -0,0 +1,126 @@
import React, { Component } from 'react';
import JSONTree from 'react-json-tree';
import stringify from 'javascript-stringify';
import getItemString from './getItemString';
import getJsonTreeTheme from './getJsonTreeTheme';
function stringifyAndShrink(val, isWideLayout) {
if (val === null) { return 'null'; }
const str = stringify(val);
if (typeof str === 'undefined') { return 'undefined'; }
if (isWideLayout) return str.length > 42 ? str.substr(0, 30) + '…' + str.substr(-10) : str;
return str.length > 22 ? `${str.substr(0, 15)}${str.substr(-5)}` : str;
}
const expandFirstLevel = (keyName, data, level) => level <= 1;
function prepareDelta(value) {
if (value && value._t === 'a') {
const res = {};
for (let key in value) {
if (key !== '_t') {
if (key[0] === '_' && !value[key.substr(1)]) {
res[key.substr(1)] = value[key];
} else if (value['_' + key]) {
res[key] = [value['_' + key][0], value[key][0]];
} else if (!value['_' + key] && key[0] !== '_') {
res[key] = value[key];
}
}
}
return res;
}
return value;
}
export default class JSONDiff extends Component {
state = { data: {} }
componentDidMount() {
this.updateData();
}
componentDidUpdate(prevProps) {
if (prevProps.delta !== this.props.delta) {
this.updateData();
}
}
updateData() {
// this magically fixes weird React error, where it can't find a node in tree
// if we set `delta` as JSONTree data right away
// https://github.com/alexkuz/redux-devtools-inspector/issues/17
this.setState({ data: this.props.delta });
}
render() {
const { styling, base16Theme, ...props } = this.props;
if (!this.state.data) {
return (
<div {...styling('stateDiffEmpty')}>
(states are equal)
</div>
);
}
return (
<JSONTree {...props}
theme={getJsonTreeTheme(base16Theme)}
data={this.state.data}
getItemString={this.getItemString}
valueRenderer={this.valueRenderer}
postprocessValue={prepareDelta}
isCustomNode={Array.isArray}
shouldExpandNode={expandFirstLevel}
hideRoot />
);
}
getItemString = (type, data) => (
getItemString(
this.props.styling, type, data, this.props.dataTypeKey, this.props.isWideLayout, true
)
)
valueRenderer = (raw, value) => {
const { styling, isWideLayout } = this.props;
function renderSpan(name, body) {
return (
<span key={name} {...styling(['diff', name])}>{body}</span>
);
}
if (Array.isArray(value)) {
switch(value.length) {
case 1:
return (
<span {...styling('diffWrap')}>
{renderSpan('diffAdd', stringifyAndShrink(value[0], isWideLayout))}
</span>
);
case 2:
return (
<span {...styling('diffWrap')}>
{renderSpan('diffUpdateFrom', stringifyAndShrink(value[0], isWideLayout))}
{renderSpan('diffUpdateArrow', ' => ')}
{renderSpan('diffUpdateTo', stringifyAndShrink(value[1], isWideLayout))}
</span>
);
case 3:
return (
<span {...styling('diffWrap')}>
{renderSpan('diffRemove', stringifyAndShrink(value[0]))}
</span>
);
}
}
return raw;
}
}

View File

@ -0,0 +1,18 @@
import React from 'react';
import JSONTree from 'react-json-tree';
import getItemString from './getItemString';
import getJsonTreeTheme from './getJsonTreeTheme';
const StateTab = ({
nextState, styling, base16Theme, invertTheme, labelRenderer, dataTypeKey, isWideLayout
}) =>
(<JSONTree
labelRenderer={labelRenderer}
theme={getJsonTreeTheme(base16Theme)}
data={nextState}
getItemString={(type, data) => getItemString(styling, type, data, dataTypeKey, isWideLayout)}
invertTheme={invertTheme}
hideRoot
/>);
export default StateTab;

View File

@ -0,0 +1,70 @@
import React from 'react';
import { Iterable } from 'immutable';
import isIterable from '../utils/isIterable';
const IS_IMMUTABLE_KEY = '@@__IS_IMMUTABLE__@@';
function isImmutable(value) {
return Iterable.isKeyed(value) || Iterable.isIndexed(value) || Iterable.isIterable(value);
}
function getShortTypeString(val, diff) {
if (diff && Array.isArray(val)) {
val = val[val.length === 2 ? 1 : 0];
}
if (isIterable(val) && !isImmutable(val)) {
return '(…)';
} else if (Array.isArray(val)) {
return val.length > 0 ? '[…]' : '[]';
} else if (val === null) {
return 'null';
} else if (val === undefined) {
return 'undef';
} else if (typeof val === 'object') {
return Object.keys(val).length > 0 ? '{…}' : '{}';
} else if (typeof val === 'function') {
return 'fn';
} else if (typeof val === 'string') {
return `"${val.substr(0, 10) + (val.length > 10 ? '…' : '')}"`
} else if (typeof val === 'symbol') {
return 'symbol'
} else {
return val;
}
}
function getText(type, data, isWideLayout, isDiff) {
if (type === 'Object') {
const keys = Object.keys(data);
if (!isWideLayout) return keys.length ? '{…}' : '{}';
const str = keys
.slice(0, 3)
.map(key => `${key}: ${getShortTypeString(data[key], isDiff)}`)
.concat(keys.length > 3 ? ['…'] : [])
.join(', ');
return `{ ${str} }`;
} else if (type === 'Array') {
if (!isWideLayout) return data.length ? '[…]' : '[]';
const str = data
.slice(0, 4)
.map(val => getShortTypeString(val, isDiff))
.concat(data.length > 4 ? ['…'] : []).join(', ');
return `[${str}]`;
} else {
return type;
}
}
const getItemString = (styling, type, data, dataTypeKey, isWideLayout, isDiff) =>
(<span {...styling('treeItemHint')}>
{data[IS_IMMUTABLE_KEY] ? 'Immutable' : ''}
{dataTypeKey && data[dataTypeKey] ? data[dataTypeKey] + ' ' : ''}
{getText(type, data, isWideLayout, isDiff)}
</span>);
export default getItemString;

View File

@ -0,0 +1,17 @@
export default function getJsonTreeTheme(base16Theme) {
return {
extend: base16Theme,
nestedNode: ({ style }, keyPath, nodeType, expanded) => ({
style: {
...style,
whiteSpace: expanded ? 'inherit' : 'nowrap'
}
}),
nestedNodeItemString: ({ style }, keyPath, nodeType, expanded) => ({
style: {
...style,
display: expanded ? 'none' : 'inline'
}
})
};
}

View File

@ -0,0 +1 @@
export { default as inspector } from './inspector';

View File

@ -0,0 +1,20 @@
export default {
scheme: 'inspector',
author: 'Alexander Kuznetsov (alexkuz@gmail.com)',
base00: '#181818',
base01: '#282828',
base02: '#383838',
base03: '#585858',
base04: '#b8b8b8',
base05: '#d8d8d8',
base06: '#e8e8e8',
base07: '#FFFFFF',
base08: '#E92F28',
base09: '#dc9656',
base0A: '#f7ca88',
base0B: '#65AD00',
base0C: '#86c1b9',
base0D: '#347BD9',
base0E: '#EC31C0',
base0F: '#a16946'
};

View File

@ -0,0 +1,412 @@
import jss from 'jss';
import jssVendorPrefixer from 'jss-vendor-prefixer';
import jssNested from 'jss-nested';
import { createStyling } from 'react-base16-styling';
import rgba from 'hex-rgba';
import inspector from '../themes/inspector';
import * as reduxThemes from 'redux-devtools-themes';
import * as inspectorThemes from '../themes';
jss.use(jssVendorPrefixer());
jss.use(jssNested());
const colorMap = theme => ({
TEXT_COLOR: theme.base06,
TEXT_PLACEHOLDER_COLOR: rgba(theme.base06, 60),
BACKGROUND_COLOR: theme.base00,
SELECTED_BACKGROUND_COLOR: rgba(theme.base03, 20),
SKIPPED_BACKGROUND_COLOR: rgba(theme.base03, 10),
HEADER_BACKGROUND_COLOR: rgba(theme.base03, 30),
HEADER_BORDER_COLOR: rgba(theme.base03, 20),
BORDER_COLOR: rgba(theme.base03, 50),
LIST_BORDER_COLOR: rgba(theme.base03, 50),
ACTION_TIME_BACK_COLOR: rgba(theme.base03, 20),
ACTION_TIME_COLOR: theme.base04,
PIN_COLOR: theme.base04,
ITEM_HINT_COLOR: rgba(theme.base0F, 90),
TAB_BACK_SELECTED_COLOR: rgba(theme.base03, 20),
TAB_BACK_COLOR: rgba(theme.base00, 70),
TAB_BACK_HOVER_COLOR: rgba(theme.base03, 40),
TAB_BORDER_COLOR: rgba(theme.base03, 50),
DIFF_ADD_COLOR: rgba(theme.base0B, 40),
DIFF_REMOVE_COLOR: rgba(theme.base08, 40),
DIFF_ARROW_COLOR: theme.base0E,
LINK_COLOR: rgba(theme.base0E, 90),
LINK_HOVER_COLOR: theme.base0E,
ERROR_COLOR: theme.base08,
});
const getSheetFromColorMap = map => ({
inspector: {
display: 'flex',
'flex-direction': 'column',
width: '100%',
height: '100%',
'font-family': 'monaco, Consolas, "Lucida Console", monospace',
'font-size': '12px',
'font-smoothing': 'antialiased',
'line-height': '1.5em',
'background-color': map.BACKGROUND_COLOR,
color: map.TEXT_COLOR
},
inspectorWide: {
'flex-direction': 'row'
},
actionList: {
'flex-basis': '40%',
'flex-shrink': 0,
'overflow-x': 'hidden',
'overflow-y': 'auto',
'border-bottom-width': '3px',
'border-bottom-style': 'double',
display: 'flex',
'flex-direction': 'column',
'background-color': map.BACKGROUND_COLOR,
'border-color': map.LIST_BORDER_COLOR
},
actionListHeader: {
display: 'flex',
flex: '0 0 auto',
'align-items': 'center',
'border-bottom-width': '1px',
'border-bottom-style': 'solid',
'border-color': map.LIST_BORDER_COLOR
},
actionListRows: {
overflow: 'auto',
'& div.gu-transit': {
opacity: '0.3'
},
'& div.gu-mirror': {
position: 'fixed',
opacity: '0.8',
height: 'auto !important',
'border-width': '1px',
'border-style': 'solid',
'border-color': map.LIST_BORDER_COLOR
},
'& div.gu-hide': {
display: 'none'
}
},
actionListHeaderSelector: {
display: 'inline-flex',
'margin-right': '10px'
},
actionListWide: {
'flex-basis': '40%',
'border-bottom': 'none',
'border-right-width': '3px',
'border-right-style': 'double'
},
actionListItem: {
'border-bottom-width': '1px',
'border-bottom-style': 'solid',
display: 'flex',
'justify-content': 'space-between',
padding: '5px 10px',
cursor: 'pointer',
'user-select': 'none',
'&:last-child': {
'border-bottom-width': 0
},
'border-bottom-color': map.BORDER_COLOR
},
actionListItemSelected: {
'background-color': map.SELECTED_BACKGROUND_COLOR
},
actionListItemSkipped: {
'background-color': map.SKIPPED_BACKGROUND_COLOR
},
actionListFromFuture: {
opacity: '0.6'
},
actionListItemButtons: {
position: 'relative',
height: '20px',
display: 'flex'
},
actionListItemTime: {
display: 'inline',
padding: '4px 6px',
'border-radius': '3px',
'font-size': '0.8em',
'line-height': '1em',
'flex-shrink': 0,
'background-color': map.ACTION_TIME_BACK_COLOR,
color: map.ACTION_TIME_COLOR
},
actionListItemSelector: {
display: 'inline-flex'
},
actionListItemName: {
overflow: 'hidden',
'text-overflow': 'ellipsis',
'line-height': '20px'
},
actionListItemNameSkipped: {
'text-decoration': 'line-through',
opacity: 0.3
},
actionListHeaderSearch: {
outline: 'none',
border: 'none',
width: '100%',
padding: '5px 10px',
'font-size': '1em',
'font-family': 'monaco, Consolas, "Lucida Console", monospace',
'background-color': map.BACKGROUND_COLOR,
color: map.TEXT_COLOR,
'&::-webkit-input-placeholder': {
color: map.TEXT_PLACEHOLDER_COLOR
},
'&::-moz-placeholder': {
color: map.TEXT_PLACEHOLDER_COLOR
}
},
actionListHeaderWrapper: {
position: 'relative',
height: '20px'
},
actionPreview: {
flex: 1,
display: 'flex',
'flex-direction': 'column',
'flex-grow': 1,
'overflow-y': 'hidden',
'& pre': {
border: 'inherit',
'border-radius': '3px',
'line-height': 'inherit',
color: 'inherit'
},
'background-color': map.BACKGROUND_COLOR,
},
actionPreviewContent: {
flex: 1,
'overflow-y': 'auto'
},
stateDiff: {
padding: '5px 0'
},
stateDiffEmpty: {
padding: '10px',
color: map.TEXT_PLACEHOLDER_COLOR
},
stateError: {
padding: '10px',
'margin-left': '14px',
'font-weight': 'bold',
color: map.ERROR_COLOR
},
inspectedPath: {
padding: '6px 0'
},
inspectedPathKey: {
'&:not(:last-child):after': {
content: '" > "'
}
},
inspectedPathKeyLink: {
cursor: 'pointer',
'&:hover': {
'text-decoration': 'underline'
},
color: map.LINK_COLOR,
'&:hover': {
color: map.LINK_HOVER_COLOR
}
},
treeItemPin: {
'font-size': '0.7em',
'padding-left': '5px',
cursor: 'pointer',
'&:hover': {
'text-decoration': 'underline'
},
color: map.PIN_COLOR
},
treeItemHint: {
color: map.ITEM_HINT_COLOR
},
previewHeader: {
flex: '0 0 30px',
padding: '5px 10px',
'align-items': 'center',
'border-bottom-width': '1px',
'border-bottom-style': 'solid',
'background-color': map.HEADER_BACKGROUND_COLOR,
'border-bottom-color': map.HEADER_BORDER_COLOR
},
tabSelector: {
position: 'relative',
'z-index': 1,
display: 'inline-flex',
float: 'right'
},
selectorButton: {
cursor: 'pointer',
position: 'relative',
padding: '5px 10px',
'border-style': 'solid',
'border-width': '1px',
'border-left-width': 0,
'&:first-child': {
'border-left-width': '1px',
'border-top-left-radius': '3px',
'border-bottom-left-radius': '3px'
},
'&:last-child': {
'border-top-right-radius': '3px',
'border-bottom-right-radius': '3px'
},
'background-color': map.TAB_BACK_COLOR,
'&:hover': {
'background-color': map.TAB_BACK_HOVER_COLOR
},
'border-color': map.TAB_BORDER_COLOR
},
selectorButtonSmall: {
padding: '0px 8px',
'font-size': '0.8em'
},
selectorButtonSelected: {
'background-color': map.TAB_BACK_SELECTED_COLOR
},
diff: {
padding: '2px 3px',
'border-radius': '3px',
position: 'relative',
color: map.TEXT_COLOR
},
diffWrap: {
position: 'relative',
'z-index': 1
},
diffAdd: {
'background-color': map.DIFF_ADD_COLOR
},
diffRemove: {
'text-decoration': 'line-through',
'background-color': map.DIFF_REMOVE_COLOR
},
diffUpdateFrom: {
'text-decoration': 'line-through',
'background-color': map.DIFF_REMOVE_COLOR
},
diffUpdateTo: {
'background-color': map.DIFF_ADD_COLOR
},
diffUpdateArrow: {
color: map.DIFF_ARROW_COLOR
},
rightSlider: {
'font-smoothing': 'subpixel-antialiased', // http://stackoverflow.com/a/21136111/4218591
position: 'absolute',
right: 0,
transform: 'translateX(150%)',
transition: 'transform 0.2s ease-in-out'
},
rightSliderRotate: {
transform: 'rotateX(90deg)',
transition: 'transform 0.2s ease-in-out 0.08s'
},
rightSliderShown: {
position: 'static',
transform: 'translateX(0)',
},
rightSliderRotateShown: {
transform: 'rotateX(0)',
transition: 'transform 0.2s ease-in-out 0.18s'
}
});
let themeSheet;
const getDefaultThemeStyling = theme => {
if (themeSheet) {
themeSheet.detach();
}
themeSheet = jss.createStyleSheet(
getSheetFromColorMap(colorMap(theme))
).attach();
return themeSheet.classes;
};
export const base16Themes = { ...reduxThemes, ...inspectorThemes };
export const createStylingFromTheme = createStyling(getDefaultThemeStyling, {
defaultBase16: inspector,
base16Themes
});

View File

@ -0,0 +1,29 @@
function deepMapCached(obj, f, ctx, cache) {
cache.push(obj);
if (Array.isArray(obj)) {
return obj.map(function(val, key) {
val = f.call(ctx, val, key);
return (typeof val === 'object' && cache.indexOf(val) === -1) ?
deepMapCached(val, f, ctx, cache) : val;
});
} else if (typeof obj === 'object') {
const res = {};
for (const key in obj) {
let val = obj[key];
if (val && typeof val === 'object') {
val = f.call(ctx, val, key);
res[key] = cache.indexOf(val) === -1 ?
deepMapCached(val, f, ctx, cache) : val;
} else {
res[key] = f.call(ctx, val, key);
}
}
return res;
} else {
return obj;
}
}
export default function deepMap(obj, f, ctx) {
return deepMapCached(obj, f, ctx, []);
}

View File

@ -0,0 +1,45 @@
import { Iterable, fromJS } from 'immutable';
import isIterable from './isIterable';
function iterateToKey(obj, key) { // maybe there's a better way, dunno
let idx = 0;
for (let entry of obj) {
if (Array.isArray(entry)) {
if (entry[0] === key) return entry[1];
} else {
if (idx > key) return;
if (idx === key) return entry;
}
idx++;
}
}
export default function getInspectedState(state, path, convertImmutable) {
state = path && path.length ?
{
[path[path.length - 1]]: path.reduce(
(s, key) => {
if (!s) {
return s;
}
if (Iterable.isAssociative(s)) {
return s.get(key);
} else if (isIterable(s)) {
return iterateToKey(s, key);
}
return s[key];
},
state
)
} : state;
if (convertImmutable) {
try {
state = fromJS(state).toJS();
} catch(e) {}
}
return state;
}

View File

@ -0,0 +1,4 @@
export default function isIterable(obj) {
return obj !== null && typeof obj === 'object' && !Array.isArray(obj) &&
typeof obj[window.Symbol.iterator] === 'function';
}

View File

@ -0,0 +1,76 @@
var path = require('path');
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var CleanWebpackPlugin = require('clean-webpack-plugin');
var ExportFilesWebpackPlugin = require('export-files-webpack-plugin');
var NyanProgressWebpackPlugin = require('nyan-progress-webpack-plugin');
var pkg = require('./package.json');
var isProduction = process.env.NODE_ENV === 'production';
module.exports = {
devtool: 'eval',
entry: isProduction ?
[ './demo/src/js/index' ] :
[
'webpack-dev-server/client?http://localhost:3000',
'webpack/hot/only-dev-server',
'./demo/src/js/index'
],
output: {
path: path.join(__dirname, 'demo/dist'),
filename: 'js/bundle.js',
hash: true
},
plugins: [
new CleanWebpackPlugin(isProduction ? ['demo/dist'] : []),
new HtmlWebpackPlugin({
inject: true,
template: 'demo/src/index.html',
filename: 'index.html',
package: pkg
}),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV)
},
}),
new webpack.NoErrorsPlugin(),
new NyanProgressWebpackPlugin()
].concat(isProduction ? [
new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false },
output: { comments: false }
})
] : [
new ExportFilesWebpackPlugin('demo/dist/index.html'),
new webpack.HotModuleReplacementPlugin()
]),
resolve: {
extensions: ['', '.js', '.jsx']
},
module: {
loaders: [{
test: /\.jsx?$/,
loaders: ['babel'],
include: [
path.join(__dirname, 'src'),
path.join(__dirname, 'demo/src/js')
]
}, {
test: /\.json$/,
loader: 'json'
}]
},
devServer: isProduction ? null : {
quiet: false,
port: 3000,
hot: true,
stats: {
chunkModules: false,
colors: true
},
historyApiFallback: true
}
};

1384
yarn.lock

File diff suppressed because it is too large Load Diff