mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2024-11-25 11:03:57 +03:00
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:
parent
4187bc1797
commit
89880265a6
15
packages/redux-devtools-inspector/.babelrc
Normal file
15
packages/redux-devtools-inspector/.babelrc
Normal 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"]
|
||||||
|
}]
|
||||||
|
}]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
57
packages/redux-devtools-inspector/.eslintrc
Normal file
57
packages/redux-devtools-inspector/.eslintrc
Normal 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
|
||||||
|
}
|
||||||
|
}
|
8
packages/redux-devtools-inspector/.npmignore
Normal file
8
packages/redux-devtools-inspector/.npmignore
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
static
|
||||||
|
src
|
||||||
|
demo
|
||||||
|
.*
|
||||||
|
webpack.config.js
|
||||||
|
index.html
|
||||||
|
*.gif
|
||||||
|
*.png
|
21
packages/redux-devtools-inspector/LICENSE
Normal file
21
packages/redux-devtools-inspector/LICENSE
Normal 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.
|
68
packages/redux-devtools-inspector/README.md
Normal file
68
packages/redux-devtools-inspector/README.md
Normal 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
|
BIN
packages/redux-devtools-inspector/demo.gif
Normal file
BIN
packages/redux-devtools-inspector/demo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"import": true
|
||||||
|
}
|
14
packages/redux-devtools-inspector/demo/src/index.html
Normal file
14
packages/redux-devtools-inspector/demo/src/index.html
Normal 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>
|
248
packages/redux-devtools-inspector/demo/src/js/DemoApp.jsx
Normal file
248
packages/redux-devtools-inspector/demo/src/js/DemoApp.jsx
Normal 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);
|
11
packages/redux-devtools-inspector/demo/src/js/getOptions.js
Normal file
11
packages/redux-devtools-inspector/demo/src/js/getOptions.js
Normal 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
|
||||||
|
};
|
||||||
|
}
|
100
packages/redux-devtools-inspector/demo/src/js/index.js
Normal file
100
packages/redux-devtools-inspector/demo/src/js/index.js
Normal 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());
|
123
packages/redux-devtools-inspector/demo/src/js/reducers.js
Normal file
123
packages/redux-devtools-inspector/demo/src/js/reducers.js
Normal 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
|
||||||
|
};
|
92
packages/redux-devtools-inspector/package.json
Normal file
92
packages/redux-devtools-inspector/package.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
3
packages/redux-devtools-inspector/src/.noderequirer.json
Normal file
3
packages/redux-devtools-inspector/src/.noderequirer.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"import": true
|
||||||
|
}
|
124
packages/redux-devtools-inspector/src/ActionList.jsx
Normal file
124
packages/redux-devtools-inspector/src/ActionList.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
43
packages/redux-devtools-inspector/src/ActionListHeader.jsx
Normal file
43
packages/redux-devtools-inspector/src/ActionListHeader.jsx
Normal 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;
|
129
packages/redux-devtools-inspector/src/ActionListRow.jsx
Normal file
129
packages/redux-devtools-inspector/src/ActionListRow.jsx
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
97
packages/redux-devtools-inspector/src/ActionPreview.jsx
Normal file
97
packages/redux-devtools-inspector/src/ActionPreview.jsx
Normal 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;
|
|
@ -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;
|
270
packages/redux-devtools-inspector/src/DevtoolsInspector.js
Normal file
270
packages/redux-devtools-inspector/src/DevtoolsInspector.js
Normal 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 });
|
||||||
|
};
|
||||||
|
}
|
18
packages/redux-devtools-inspector/src/RightSlider.jsx
Normal file
18
packages/redux-devtools-inspector/src/RightSlider.jsx
Normal 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;
|
29
packages/redux-devtools-inspector/src/createDiffPatcher.js
Normal file
29
packages/redux-devtools-inspector/src/createDiffPatcher.js
Normal 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
|
||||||
|
});
|
||||||
|
}
|
1
packages/redux-devtools-inspector/src/index.js
Normal file
1
packages/redux-devtools-inspector/src/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export default from './DevtoolsInspector';
|
26
packages/redux-devtools-inspector/src/redux.js
Normal file
26
packages/redux-devtools-inspector/src/redux.js
Normal 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)
|
||||||
|
};
|
||||||
|
}
|
18
packages/redux-devtools-inspector/src/tabs/ActionTab.jsx
Normal file
18
packages/redux-devtools-inspector/src/tabs/ActionTab.jsx
Normal 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;
|
9
packages/redux-devtools-inspector/src/tabs/DiffTab.jsx
Normal file
9
packages/redux-devtools-inspector/src/tabs/DiffTab.jsx
Normal 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;
|
126
packages/redux-devtools-inspector/src/tabs/JSONDiff.jsx
Normal file
126
packages/redux-devtools-inspector/src/tabs/JSONDiff.jsx
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
18
packages/redux-devtools-inspector/src/tabs/StateTab.jsx
Normal file
18
packages/redux-devtools-inspector/src/tabs/StateTab.jsx
Normal 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;
|
70
packages/redux-devtools-inspector/src/tabs/getItemString.js
Normal file
70
packages/redux-devtools-inspector/src/tabs/getItemString.js
Normal 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;
|
|
@ -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'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
1
packages/redux-devtools-inspector/src/themes/index.js
Normal file
1
packages/redux-devtools-inspector/src/themes/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default as inspector } from './inspector';
|
20
packages/redux-devtools-inspector/src/themes/inspector.js
Normal file
20
packages/redux-devtools-inspector/src/themes/inspector.js
Normal 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'
|
||||||
|
};
|
|
@ -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
|
||||||
|
});
|
29
packages/redux-devtools-inspector/src/utils/deepMap.js
Normal file
29
packages/redux-devtools-inspector/src/utils/deepMap.js
Normal 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, []);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default function isIterable(obj) {
|
||||||
|
return obj !== null && typeof obj === 'object' && !Array.isArray(obj) &&
|
||||||
|
typeof obj[window.Symbol.iterator] === 'function';
|
||||||
|
}
|
76
packages/redux-devtools-inspector/webpack.config.js
Normal file
76
packages/redux-devtools-inspector/webpack.config.js
Normal 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
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user