mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-01-31 11:51:41 +03:00
Merge pull request #445 from reduxjs/redux-devtools-app
Move remotedev-app and remotedev-server
This commit is contained in:
commit
e67bc5a622
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -3,5 +3,7 @@ node_modules
|
|||
.DS_Store
|
||||
lib
|
||||
dist
|
||||
umd
|
||||
build
|
||||
coverage
|
||||
.idea
|
||||
|
|
|
@ -16,6 +16,7 @@ It can be used as a browser extension (for [Chrome](https://chrome.google.com/we
|
|||
- [Manual Integration as a React Component](./docs/Walkthrough.md#manual-integration)
|
||||
- [Extension Options (Arguments)](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/API/Arguments.md)
|
||||
- [Extension Methods (Advanced API)](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/API/Methods.md)
|
||||
- [Remote monitoring](./docs/Integrations/Remote.md)
|
||||
- [Troubleshooting](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/Troubleshooting.md)
|
||||
- [Recipes](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/Recipes.md)
|
||||
- [FAQ](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/FAQ.md)
|
||||
|
|
157
docs/Integrations/Remote.md
Normal file
157
docs/Integrations/Remote.md
Normal file
|
@ -0,0 +1,157 @@
|
|||
## Remote monitoring
|
||||
|
||||
By installing [`redux-devtools-cli`](https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools-cli#usage), starting the server server and launching the Redux DevTools app (`redux-devtools --open`), you can connect any remote application, even not javascript. There are some integrations for javascript like [remote-redux-devtools](https://github.com/zalmoxisus/remote-redux-devtools) and [remotedev](https://github.com/zalmoxisus/remotedev), but the plan is to deprecate them and support it out of the box from the extension without a websocket server. It is more useful for non-js apps.
|
||||
|
||||
### WebSocket Clients
|
||||
|
||||
We're using [SocketCluster](http://socketcluster.io/) for realtime communication, which provides a fast and scalable webSocket layer and a minimal pub/sub system. You need to include one of [its clients](https://github.com/SocketCluster/client-drivers) in your app to communicate with RemotedevServer. Currently there are clients for [JavaScript (NodeJS)](https://github.com/SocketCluster/socketcluster-client), [Java](https://github.com/sacOO7/socketcluster-client-java), [Python](https://github.com/sacOO7/socketcluster-client-python), [C](https://github.com/sacOO7/socketcluster-client-C), [Objective-C](https://github.com/abpopov/SocketCluster-ios-client) and [.NET/C#](https://github.com/sacOO7/SocketclusterClientDotNet).
|
||||
|
||||
By default, the websocket server is running on `ws://localhost:8000/socketcluster/`.
|
||||
|
||||
### Messaging lifecycle
|
||||
|
||||
#### 1. Connecting to the WebSocket server
|
||||
|
||||
The client driver provides a way to connect to the server via websockets (see the docs for the selected client).
|
||||
|
||||
##### JavaScript
|
||||
```js
|
||||
var socket = socketCluster.connect({
|
||||
hostname: 'localhost',
|
||||
port: 8000
|
||||
});
|
||||
```
|
||||
|
||||
##### Python
|
||||
```py
|
||||
socket = Socketcluster.socket("ws://localhost:8000/socketcluster/")
|
||||
socket.connect()
|
||||
```
|
||||
|
||||
> Note that JavaScript client composes the url from `hostname` and `port`, adding `/socketcluster/` path automatically. For other clients, you should specify that path. For example, for `ObjectiveC` it would be `self.client.initWithHost("localhost/socketcluster/", onPort: 8000, securely: false)`.
|
||||
|
||||
#### 2. Disconnecting and reconnecting
|
||||
|
||||
SocketCluster client handles reconnecting for you, but you still might want to know when the connection is established, or when it failed to connect.
|
||||
|
||||
##### JavaScript
|
||||
```js
|
||||
socket.on('connect', status => {
|
||||
// Here will come the next step
|
||||
});
|
||||
socket.on('disconnect', code => {
|
||||
console.warn('Socket disconnected with code', code);
|
||||
});
|
||||
socket.on('error', error => {
|
||||
console.warn('Socket error', error);
|
||||
});
|
||||
```
|
||||
|
||||
##### Python
|
||||
```py
|
||||
def onconnect(socket):
|
||||
// Here will call the next step
|
||||
|
||||
def ondisconnect(socket):
|
||||
logging.info("on disconnect got called")
|
||||
|
||||
def onConnectError(socket, error):
|
||||
logging.info("On connect error got called")
|
||||
|
||||
socket.setBasicListener(onconnect, ondisconnect, onConnectError)
|
||||
```
|
||||
|
||||
#### 3. Authorizing and subscribing to the channel of events
|
||||
|
||||
We're not providing an authorizing mechanism yet. All you have to do is to emit a `login` event, and you'll get a `channelName` you should subscribe for, and watch for messages and events. Make sure to pass the `master` event, otherwise it should be a monitor, not a client app.
|
||||
|
||||
##### JavaScript
|
||||
```js
|
||||
socket.emit('login', 'master', (error, channelName) => {
|
||||
if (error) { console.log(error); return; }
|
||||
channel = socket.subscribe(channelName);
|
||||
channel.watch(handleMessages);
|
||||
socket.on(channelName, handleMessages);
|
||||
});
|
||||
|
||||
function handleMessages(message) {
|
||||
// 5. Listening for monitor events
|
||||
}
|
||||
```
|
||||
|
||||
##### Python
|
||||
```py
|
||||
socket.emitack("login", "master", login)
|
||||
|
||||
def login(key, error, channelName):
|
||||
socket.subscribe(channelName)
|
||||
socket.onchannel(channelName, handleMessages)
|
||||
socket.on(channelName, handleMessages)
|
||||
|
||||
def handleMessages(key, message):
|
||||
// 5. Listening for monitor events
|
||||
```
|
||||
|
||||
You could just emit the `login` event, and omit subscribing (and point `5` bellow) if you want only to log data, not to interact with te app.
|
||||
|
||||
#### 4. Sending the action and state to the monitor
|
||||
|
||||
To send your data to the monitor use `log` or `log-noid` channel. The latter will add the socket id to the message from the server side (useful when the message was sent before the connection was established).
|
||||
|
||||
The message object includes the following:
|
||||
- `type` - usually should be `ACTION`. If you want to indicate that we're starting a new log (clear all actions emitted before and add `@@INIT`), use `INIT`. In case you have a lifted state similar to one provided by [`redux-devtools-instrument`](https://github.com/zalmoxisus/redux-devtools-instrument), use `STATE`.
|
||||
- `action` - the action object. It is recommended to lift it in another object, and add `timestamp` to show when the action was fired off: `{ timestamp: Date.now(), action: { type: 'SOME_ACTION' } }`.
|
||||
- `payload` - usually the state or lifted state object.
|
||||
- `name` - name of the instance to be shown in the instances selector. If not provided, it will be equal to `instanceId`.
|
||||
- `instanceId` - an id to identify the instance. If not provided, it will be the same as `id`. However, it is useful when having several instances (or stores) in the same connection. Also if the user will specify a constant value, it would allow to persist the state on app reload.
|
||||
- `id` - socket connection id, which should be either `socket.id` or should not provided and use `log-noid` channel.
|
||||
|
||||
##### JavaScript
|
||||
```js
|
||||
const message = {
|
||||
type: 'ACTION',
|
||||
action: { action, timestamp: Date.now() },
|
||||
payload: state,
|
||||
id: socket.id,
|
||||
instanceId: window.btoa(location.href),
|
||||
name: document.title
|
||||
};
|
||||
socket.emit(socket.id ? 'log' : 'log-noid', message);
|
||||
```
|
||||
|
||||
##### Python
|
||||
```py
|
||||
class Message:
|
||||
def __init__(self, action, state):
|
||||
self.type = "ACTION"
|
||||
self.action = action
|
||||
self.payload = state
|
||||
id: socket.id
|
||||
socket.emit(socket.id if "log" else "log-noid", Message(action, state));
|
||||
```
|
||||
|
||||
#### 5. Listening for monitor events
|
||||
|
||||
When a monitor action is emitted, you'll get an event on the subscribed function. The argument object includes a `type` key, which can be:
|
||||
- `DISPATCH` - a monitor action dispatched on Redux DevTools monitor, like `{ type: 'DISPATCH', payload: { type: 'JUMP_TO_STATE', 'index': 2 }`. See [`redux-devtools-instrument`](https://github.com/zalmoxisus/redux-devtools-instrument/blob/master/src/instrument.js) for details. Additionally to that API, you'll get also a stringified `state` object when needed. So, for example, for time travelling (`JUMP_TO_STATE`) you can just parse and set the state (see the example). Usually implementing this type of actions would be enough.
|
||||
- `ACTION` - the user requested to dispatch an action remotely like `{ type: 'ACTION', action: '{ type: \'INCREMENT_COUNTER\' }' }`. The `action` can be either a stringified javascript object which should be evalled or a function which arguments should be evalled like [here](https://github.com/zalmoxisus/remotedev-utils/blob/master/src/index.js#L62-L70).
|
||||
- `START` - a monitor was opened. You could handle this event in order not to do extra tasks when the app is not monitored.
|
||||
- `STOP` - a monitor was closed. You can take this as no need to send data to the monitor. I there are several monitors and one was closed, all others will send `START` event to acknowledge that we still have to send data.
|
||||
|
||||
See [`mobx-remotedev`](https://github.com/zalmoxisus/mobx-remotedev/blob/master/src/monitorActions.js) for an example of implementation without [`redux-devtools-instrument`](https://github.com/zalmoxisus/redux-devtools-instrument/blob/master/src/instrument.js).
|
||||
|
||||
##### JavaScript
|
||||
```js
|
||||
function handleMessages(message) {
|
||||
if (message.type === 'DISPATCH' && message.payload.type === 'JUMP_TO_STATE') {
|
||||
store.setState(JSON.parse(message.state));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Python
|
||||
```py
|
||||
def handleMessages(key, message):
|
||||
if message.type === "DISPATCH" and message.payload.type === "JUMP_TO_STATE":
|
||||
store.setState(json.loads(message.state));
|
||||
```
|
|
@ -4,11 +4,12 @@
|
|||
"babel-eslint": "^10.0.0",
|
||||
"eslint-plugin-react": "7.4.0",
|
||||
"eslint-plugin-flowtype": "3.2.0",
|
||||
"lerna": "3.4.2"
|
||||
"lerna": "3.4.2",
|
||||
"pre-commit": "^1.1.3"
|
||||
},
|
||||
"scripts": {
|
||||
"lerna": "lerna",
|
||||
"prepare": "lerna run prepare --since master --stream --sort -- --scripts-prepend-node-path",
|
||||
"build": "lerna run prepare --since master --stream --sort -- --scripts-prepend-node-path",
|
||||
"build:all": "lerna run build",
|
||||
"publish": "lerna publish",
|
||||
"canary": "lerna publish --canary preminor --npm-tag alpha",
|
||||
|
@ -20,5 +21,8 @@
|
|||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"pre-commit": [
|
||||
"lint"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export const ADDON_ID = 'remotedev-themes-storybook';
|
||||
export const ADDON_ID = 'redux-devtools-themes-storybook';
|
||||
export const PANEL_ID = `${ADDON_ID}/panel`;
|
||||
export const EVENT_ID_DATA = `${ADDON_ID}/event/data`;
|
||||
export const CSS_CLASS = 'remotedev-storybook';
|
||||
export const CSS_CLASS = 'redux-devtools-storybook';
|
||||
export const DEFAULT_THEME_STATE = {
|
||||
theme: 'default',
|
||||
scheme: 'default',
|
||||
|
|
|
@ -89,7 +89,6 @@
|
|||
"simple-element-resize-detector": "^1.1.0",
|
||||
"styled-components": "^2.2.2"
|
||||
},
|
||||
"pre-commit": ["lint"],
|
||||
"jest": {
|
||||
"setupTestFrameworkScriptFile": "<rootDir>/tests/setup.js"
|
||||
},
|
||||
|
|
21
packages/redux-devtools-cli/LICENSE.md
Normal file
21
packages/redux-devtools-cli/LICENSE.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Mihail Diordiev
|
||||
|
||||
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.
|
162
packages/redux-devtools-cli/README.md
Normal file
162
packages/redux-devtools-cli/README.md
Normal file
|
@ -0,0 +1,162 @@
|
|||
Redux DevTools Command Line Interface
|
||||
=====================================
|
||||
|
||||
Bridge for remote debugging via [Redux DevTools extension](https://github.com/zalmoxisus/redux-devtools-extension), [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools) or [RemoteDev](https://github.com/zalmoxisus/remotedev).
|
||||
|
||||
### Usage
|
||||
|
||||
#### Install the package globally
|
||||
|
||||
with npm:
|
||||
|
||||
```
|
||||
npm install -g redux-devtools-cli
|
||||
```
|
||||
|
||||
or with yarn:
|
||||
|
||||
```
|
||||
yarn global add redux-devtools-cli
|
||||
```
|
||||
|
||||
and start as:
|
||||
|
||||
```
|
||||
redux-devtools --hostname=localhost --port=8000
|
||||
```
|
||||
|
||||
> Note the package is called `redux-devtools-cli` not `redux-devtools` (the latter is a React component).
|
||||
|
||||
#### Or add in your project
|
||||
|
||||
with npm:
|
||||
|
||||
```
|
||||
npm install --save-dev redux-devtools-cli
|
||||
```
|
||||
|
||||
or with yarn:
|
||||
|
||||
```
|
||||
yarn add --dev redux-devtools-cli
|
||||
```
|
||||
|
||||
and add to `package.json`:
|
||||
|
||||
```
|
||||
"scripts": {
|
||||
"redux-devtools": "redux-devtools --hostname=localhost --port=8000"
|
||||
}
|
||||
```
|
||||
|
||||
So, you can start redux-devtools server by running `npm run redux-devtools`.
|
||||
|
||||
##### Import in your `server.js` script you use for starting a development server:
|
||||
|
||||
```js
|
||||
var reduxDevTools = require('redux-devtools-cli');
|
||||
reduxDevTools({ hostname: 'localhost', port: 8000 });
|
||||
```
|
||||
|
||||
So, you can start redux-devtools server together with your dev server.
|
||||
|
||||
### Open Redux DevTools
|
||||
|
||||
You can add `--open` argument (or set it as `electron`) to open Redux DevTools as a standalone application:
|
||||
|
||||
```
|
||||
redux-devtools --open
|
||||
```
|
||||
|
||||
Set it as `browser` to open as a web app in the default browser instead:
|
||||
|
||||
```
|
||||
redux-devtools --open=browser
|
||||
```
|
||||
|
||||
To specify the browser:
|
||||
|
||||
```
|
||||
redux-devtools --open=firefox
|
||||
```
|
||||
|
||||
### Connection settings
|
||||
|
||||
Set `hostname` and `port` to the values you want. `hostname` by default is `localhost` and `port` is `8000`.
|
||||
|
||||
To use WSS, set `protocol` argument to `https` and provide `key`, `cert` and `passphrase` arguments.
|
||||
|
||||
|
||||
#### Available options
|
||||
|
||||
| Console argument | description | default value |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `--hostname` | hostname | localhost |
|
||||
| `--port` | port | 8000 |
|
||||
| `--protocol` | protocol | http |
|
||||
| `--key` | the key file for [running an https server](https://github.com/SocketCluster/socketcluster#using-over-https) (`--protocol` must be set to 'https') | - |
|
||||
| `--cert` | the cert file for [running an https server](https://github.com/SocketCluster/socketcluster#using-over-https) (`--protocol` must be set to 'https') | - |
|
||||
| `--passphrase` | the key passphrase for [running an https server](https://github.com/SocketCluster/socketcluster#using-over-https) (`--protocol` must be set to 'https') | - |
|
||||
| `--dbOptions` | database configuration, can be whether an object or a path (string) to json configuration file (by default it uses our `./defaultDbOptions.json` file. Set `migrate` key to `true` to use our migrations file. [More details bellow](#save-reports-and-logs). | - |
|
||||
| `--logLevel` | the socket server log level - 0=none, 1=error, 2=warn, 3=info | 3 |
|
||||
| `--wsEngine` | the socket server web socket engine - ws or uws (sc-uws) | ws |
|
||||
| `--open` | open Redux DevTools as a standalone application or as web app. See [Open Redux DevTools](#open-redux-devtools) for details. | false |
|
||||
|
||||
### Inject to React Native local server
|
||||
|
||||
##### Add in your React Native app's `package.json`:
|
||||
|
||||
```
|
||||
"scripts": {
|
||||
"redux-devtools": "redux-devtools --hostname=localhost --port=8000 --injectserver=reactnative"
|
||||
}
|
||||
```
|
||||
|
||||
The `injectserver` value can be `reactnative` or `macos` ([react-native-macos](https://github.com/ptmt/react-native-macos)), it used `reactnative` by default.
|
||||
|
||||
Then, we can start React Native server and Redux DevTools server with one command (`npm start`).
|
||||
|
||||
##### Revert the injection
|
||||
|
||||
Add in your React Native app's `package.json`:
|
||||
|
||||
```
|
||||
"scripts": {
|
||||
"redux-devtools-revert": "redux-devtools --revert=reactnative"
|
||||
}
|
||||
```
|
||||
|
||||
Or just run `$(npm bin)/redux-devtools --revert`.
|
||||
|
||||
### Connect from Android device or emulator
|
||||
|
||||
> Note that if you're using `injectserver` argument explained above, this step is not necessary.
|
||||
|
||||
If you're running an Android 5.0+ device connected via USB or an Android emulator, use [adb command line tool](http://developer.android.com/tools/help/adb.html) to setup port forwarding from the device to your computer:
|
||||
|
||||
```
|
||||
adb reverse tcp:8000 tcp:8000
|
||||
```
|
||||
|
||||
If you're still use Android 4.0, you should use `10.0.2.2` (Genymotion: `10.0.3.2`) instead of `localhost` in [remote-redux-devtools](https://github.com/zalmoxisus/remote-redux-devtools#storeconfigurestorejs) or [remotedev](https://github.com/zalmoxisus/remotedev#usage).
|
||||
|
||||
### Save reports and logs
|
||||
|
||||
You can store reports via [`redux-remotedev`](https://github.com/zalmoxisus/redux-remotedev) and get them replicated with [Redux DevTools extension](https://github.com/zalmoxisus/redux-devtools-extension) or [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools). You can get action history right in the extension just by clicking the link from a report. Open `http://localhost:8000/graphiql` (assuming you're using `localhost` as host and `8000`) to explore in GraphQL. Reports are posted to `http://localhost:8000/`. See examples in [tests](https://github.com/zalmoxisus/remotedev-server/blob/937cfa1f0ac9dc12ebf7068eeaa8b03022ec33bc/test/integration.spec.js#L110-L165).
|
||||
|
||||
Redux DevTools server is database agnostic using `knex` schema. By default everything is stored in the memory using sqlite database. See [`defaultDbOptions.json`](https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools-cli/defaultDbOptions.json) for example of sqlite. You can replace `"connection": { "filename": ":memory:" },` with your file name (instead of `:memory:`) to persist teh database. Here's an example for PostgreSQL:
|
||||
```
|
||||
{
|
||||
"client": "pg",
|
||||
"connection": { "user": "myuser", "password": "mypassword", "database": "mydb" },
|
||||
"debug": false,
|
||||
"migrate": true
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced
|
||||
- [Writing your integration for a native application](https://github.com/reduxjs/redux-devtools/blob/master/docs/Integrations/Remote.md)
|
||||
|
||||
### License
|
||||
|
||||
MIT
|
50
packages/redux-devtools-cli/app/electron.js
Normal file
50
packages/redux-devtools-cli/app/electron.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
// Based on https://github.com/electron/electron-quick-start
|
||||
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
const argv = require('minimist')(process.argv.slice(2));
|
||||
|
||||
let mainWindow;
|
||||
|
||||
function createWindow () {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
webSecurity: true
|
||||
}
|
||||
})
|
||||
|
||||
// mainWindow.loadFile('index.html')
|
||||
mainWindow.loadURL('http://localhost:'+ (argv.port? argv.port: 8000) );
|
||||
|
||||
// Open the DevTools.
|
||||
// mainWindow.webContents.openDevTools()
|
||||
|
||||
mainWindow.on('closed', function () {
|
||||
// Dereference the window object, usually you would store windows
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
mainWindow = null
|
||||
})
|
||||
}
|
||||
|
||||
app.on('ready', createWindow)
|
||||
|
||||
app.on('window-all-closed', function () {
|
||||
// On macOS it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (mainWindow === null) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
46
packages/redux-devtools-cli/app/index.html
Normal file
46
packages/redux-devtools-cli/app/index.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Redux DevTools</title>
|
||||
<style>
|
||||
html {
|
||||
min-width: 350px;
|
||||
min-height: 300px;
|
||||
}
|
||||
body {
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
#root > div {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="/react.production.min.js"></script>
|
||||
<script src="/react-dom.production.min.js"></script>
|
||||
<script src="/redux-devtools-core.min.js"></script>
|
||||
<script src="/port.js"></script>
|
||||
<script>
|
||||
ReactDOM.render(
|
||||
React.createElement(ReduxDevTools, {
|
||||
socketOptions: {
|
||||
hostname: location.hostname,
|
||||
port: reduxDevToolsPort,
|
||||
autoReconnect: true
|
||||
}
|
||||
}),
|
||||
document.querySelector('#root')
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
8
packages/redux-devtools-cli/app/package.json
Normal file
8
packages/redux-devtools-cli/app/package.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"private": true,
|
||||
"name": "redux-devtools-cli",
|
||||
"version": "0.0.1",
|
||||
"main": "electron.js",
|
||||
"description": "Remote Redux DevTools",
|
||||
"authors": "Mihail Diordiev"
|
||||
}
|
98
packages/redux-devtools-cli/bin/injectServer.js
Normal file
98
packages/redux-devtools-cli/bin/injectServer.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var semver = require('semver');
|
||||
|
||||
var name = 'redux-devtools-cli';
|
||||
var startFlag = '/* ' + name + ' start */';
|
||||
var endFlag = '/* ' + name + ' end */';
|
||||
var serverFlags = {
|
||||
'react-native': {
|
||||
'0.0.1': ' _server(argv, config, resolve, reject);',
|
||||
'0.31.0': " runServer(args, config, () => console.log('\\nReact packager ready.\\n'));",
|
||||
'0.44.0-rc.0': ' runServer(args, config, startedCallback, readyCallback);',
|
||||
'0.46.0-rc.0': ' runServer(runServerArgs, configT, startedCallback, readyCallback);',
|
||||
'0.57.0': ' runServer(args, configT);'
|
||||
},
|
||||
'react-native-desktop': {
|
||||
'0.0.1': ' _server(argv, config, resolve, reject);'
|
||||
}
|
||||
};
|
||||
|
||||
function getModuleVersion(modulePath) {
|
||||
return JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(modulePath, 'package.json'),
|
||||
'utf-8'
|
||||
)
|
||||
).version;
|
||||
}
|
||||
|
||||
function getServerFlag(moduleName, version) {
|
||||
var flags = serverFlags[moduleName || 'react-native'];
|
||||
var versions = Object.keys(flags);
|
||||
var flag;
|
||||
for (var i = 0; i < versions.length; i++) {
|
||||
if (semver.gte(version, versions[i])) {
|
||||
flag = flags[versions[i]];
|
||||
}
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
exports.dir = 'local-cli/server';
|
||||
exports.file = 'server.js';
|
||||
exports.fullPath = path.join(exports.dir, exports.file);
|
||||
|
||||
exports.inject = function(modulePath, options, moduleName) {
|
||||
var filePath = path.join(modulePath, exports.fullPath);
|
||||
if (!fs.existsSync(filePath)) return false;
|
||||
|
||||
var serverFlag = getServerFlag(
|
||||
moduleName,
|
||||
getModuleVersion(modulePath)
|
||||
);
|
||||
var code = [
|
||||
startFlag,
|
||||
' require("' + name + '")(' + JSON.stringify(options) + ')',
|
||||
' .then(_remotedev =>',
|
||||
' _remotedev.on("ready", () => {',
|
||||
' if (!_remotedev.portAlreadyUsed) console.log("-".repeat(80));',
|
||||
' ' + serverFlag,
|
||||
' })',
|
||||
' );',
|
||||
endFlag,
|
||||
].join('\n');
|
||||
|
||||
var serverCode = fs.readFileSync(filePath, 'utf-8');
|
||||
var start = serverCode.indexOf(startFlag); // already injected ?
|
||||
var end = serverCode.indexOf(endFlag) + endFlag.length;
|
||||
if (start === -1) {
|
||||
start = serverCode.indexOf(serverFlag);
|
||||
end = start + serverFlag.length;
|
||||
}
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
serverCode.substr(0, start) + code + serverCode.substr(end, serverCode.length)
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
exports.revert = function(modulePath, moduleName) {
|
||||
var filePath = path.join(modulePath, exports.fullPath);
|
||||
if (!fs.existsSync(filePath)) return false;
|
||||
|
||||
var serverFlag = getServerFlag(
|
||||
moduleName,
|
||||
getModuleVersion(modulePath)
|
||||
);
|
||||
var serverCode = fs.readFileSync(filePath, 'utf-8');
|
||||
var start = serverCode.indexOf(startFlag); // already injected ?
|
||||
var end = serverCode.indexOf(endFlag) + endFlag.length;
|
||||
if (start !== -1) {
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
serverCode.substr(0, start) + serverFlag + serverCode.substr(end, serverCode.length)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
};
|
30
packages/redux-devtools-cli/bin/open.js
Normal file
30
packages/redux-devtools-cli/bin/open.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
var opn = require('opn');
|
||||
var path = require('path');
|
||||
var spawn = require('cross-spawn');
|
||||
|
||||
function open(app, options) {
|
||||
if (app === true || app === 'electron') {
|
||||
try {
|
||||
spawn.sync(
|
||||
require('electron'),
|
||||
[path.join(__dirname, '..', 'app')]
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.message === 'Cannot find module \'electron\'') {
|
||||
// TODO: Move electron to dev-dependences to make our package installation faster when not needed.
|
||||
console.log(' \x1b[1;31m[Warn]\x1b[0m Electron module not installed.\n');
|
||||
/*
|
||||
We will use "npm" to install Electron via "npm install -D".
|
||||
Do you want to install 'electron' (yes/no): yes
|
||||
Installing 'electron' (running 'npm install -D webpack-cli')...
|
||||
*/
|
||||
} else {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
opn('http://localhost:' + options.port + '/', app !== 'browser' ? { app: app } : undefined);
|
||||
}
|
||||
|
||||
module.exports = open;
|
82
packages/redux-devtools-cli/bin/redux-devtools.js
Executable file
82
packages/redux-devtools-cli/bin/redux-devtools.js
Executable file
|
@ -0,0 +1,82 @@
|
|||
#! /usr/bin/env node
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var argv = require('minimist')(process.argv.slice(2));
|
||||
var chalk = require('chalk');
|
||||
var injectServer = require('./injectServer');
|
||||
var getOptions = require('./../src/options');
|
||||
var server = require('../index');
|
||||
var open = require('./open');
|
||||
|
||||
var options = getOptions(argv);
|
||||
|
||||
function readFile(filePath) {
|
||||
return fs.readFileSync(path.resolve(process.cwd(), filePath), 'utf-8');
|
||||
}
|
||||
|
||||
if (argv.protocol === 'https') {
|
||||
argv.key = argv.key ? readFile(argv.key) : null;
|
||||
argv.cert = argv.cert ? readFile(argv.cert) : null;
|
||||
}
|
||||
|
||||
function log(pass, msg) {
|
||||
var prefix = pass ? chalk.green.bgBlack('PASS') : chalk.red.bgBlack('FAIL');
|
||||
var color = pass ? chalk.blue : chalk.red;
|
||||
console.log(prefix, color(msg));
|
||||
}
|
||||
|
||||
function getModuleName(type) {
|
||||
switch (type) {
|
||||
case 'macos':
|
||||
return 'react-native-macos';
|
||||
// react-native-macos is renamed from react-native-desktop
|
||||
case 'desktop':
|
||||
return 'react-native-desktop';
|
||||
case 'reactnative':
|
||||
default:
|
||||
return 'react-native';
|
||||
}
|
||||
}
|
||||
|
||||
function getModulePath(moduleName) {
|
||||
return path.join(process.cwd(), 'node_modules', moduleName);
|
||||
}
|
||||
|
||||
function getModule(type) {
|
||||
var moduleName = getModuleName(type);
|
||||
var modulePath = getModulePath(moduleName);
|
||||
if (type === 'desktop' && !fs.existsSync(modulePath)) {
|
||||
moduleName = getModuleName('macos');
|
||||
modulePath = getModulePath(moduleName);
|
||||
}
|
||||
return {
|
||||
name: moduleName,
|
||||
path: modulePath
|
||||
};
|
||||
}
|
||||
|
||||
if (argv.revert) {
|
||||
var module = getModule(argv.revert);
|
||||
var pass = injectServer.revert(module.path, module.name);
|
||||
var msg = 'Revert injection of ReduxDevTools server from React Native local server';
|
||||
log(pass, msg + (!pass ? ', the file `' + path.join(module.name, injectServer.fullPath) + '` not found.' : '.'));
|
||||
|
||||
process.exit(pass ? 0 : 1);
|
||||
}
|
||||
|
||||
if (argv.injectserver) {
|
||||
var module = getModule(argv.injectserver);
|
||||
var pass = injectServer.inject(module.path, options, module.name);
|
||||
var msg = 'Inject ReduxDevTools server into React Native local server';
|
||||
log(pass, msg + (pass ? '.' : ', the file `' + path.join(module.name, injectServer.fullPath) + '` not found.'));
|
||||
|
||||
process.exit(pass ? 0 : 1);
|
||||
}
|
||||
|
||||
server(argv).then(function (r) {
|
||||
if (argv.open && argv.open !== 'false') {
|
||||
r.on('ready', function () {
|
||||
open(argv.open, options);
|
||||
});
|
||||
}
|
||||
});
|
13
packages/redux-devtools-cli/defaultDbOptions.json
Normal file
13
packages/redux-devtools-cli/defaultDbOptions.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"client": "sqlite3",
|
||||
"connection": { "filename": ":memory:" },
|
||||
"pool": {
|
||||
"min": 1,
|
||||
"max": 1,
|
||||
"idleTimeoutMillis": 360000000,
|
||||
"disposeTimeout": 360000000
|
||||
},
|
||||
"useNullAsDefault": true,
|
||||
"debug": false,
|
||||
"migrate": true
|
||||
}
|
40
packages/redux-devtools-cli/index.js
Normal file
40
packages/redux-devtools-cli/index.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
var getPort = require('getport');
|
||||
var SocketCluster = require('socketcluster');
|
||||
var getOptions = require('./src/options');
|
||||
|
||||
var LOG_LEVEL_NONE = 0;
|
||||
var LOG_LEVEL_ERROR = 1;
|
||||
var LOG_LEVEL_WARN = 2;
|
||||
var LOG_LEVEL_INFO = 3;
|
||||
|
||||
module.exports = function(argv) {
|
||||
var options = Object.assign(getOptions(argv), {
|
||||
workerController: __dirname + '/src/worker.js',
|
||||
allowClientPublish: false
|
||||
});
|
||||
var port = options.port;
|
||||
var logLevel = options.logLevel === undefined ? LOG_LEVEL_INFO : options.logLevel;
|
||||
return new Promise(function(resolve) {
|
||||
// Check port already used
|
||||
getPort(port, function(err, p) {
|
||||
if (err) {
|
||||
if (logLevel >= LOG_LEVEL_ERROR) {
|
||||
console.error(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (port !== p) {
|
||||
if (logLevel >= LOG_LEVEL_WARN) {
|
||||
console.log('[ReduxDevTools] Server port ' + port + ' is already used.');
|
||||
}
|
||||
resolve({ portAlreadyUsed: true, on: function(status, cb) { cb(); } });
|
||||
} else {
|
||||
if (logLevel >= LOG_LEVEL_INFO) {
|
||||
console.log('[ReduxDevTools] Start server...');
|
||||
console.log('-'.repeat(80) + '\n');
|
||||
}
|
||||
resolve(new SocketCluster(options));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
66
packages/redux-devtools-cli/package.json
Normal file
66
packages/redux-devtools-cli/package.json
Normal file
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"name": "redux-devtools-cli",
|
||||
"version": "1.0.0-1",
|
||||
"description": "CLI for remote debugging with Redux DevTools.",
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
"redux-devtools": "bin/redux-devtools.js"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"src",
|
||||
"app",
|
||||
"index.js",
|
||||
"defaultDbOptions.json"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "node ./bin/redux-devtools.js",
|
||||
"start:electron": "node ./bin/redux-devtools.js --open",
|
||||
"test": "NODE_ENV=test mocha --recursive",
|
||||
"test:watch": "NODE_ENV=test mocha --recursive --watch",
|
||||
"prepublishOnly": "npm run test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/reduxjs/redux-devtools.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
},
|
||||
"author": "Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/reduxjs/redux-devtools/issues"
|
||||
},
|
||||
"homepage": "https://github.com/reduxjs/redux-devtools",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.15.0",
|
||||
"chalk": "^1.1.3",
|
||||
"cors": "^2.7.1",
|
||||
"cross-spawn": "^6.0.5",
|
||||
"electron": "^4.0.1",
|
||||
"express": "^4.13.3",
|
||||
"getport": "^0.1.0",
|
||||
"graphql": "^0.13.0",
|
||||
"graphql-server-express": "^1.4.0",
|
||||
"graphql-tools": "^4.0.3",
|
||||
"knex": "^0.15.2",
|
||||
"lodash": "^4.15.0",
|
||||
"minimist": "^1.2.0",
|
||||
"morgan": "^1.7.0",
|
||||
"opn": "^5.4.0",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"redux-devtools-core": "^1.0.0-1",
|
||||
"semver": "^5.3.0",
|
||||
"socketcluster": "^14.3.3",
|
||||
"sqlite3": "^4.0.4",
|
||||
"uuid": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"expect": "^1.20.2",
|
||||
"mocha": "^3.2.0",
|
||||
"socketcluster-client": "^14.0.0",
|
||||
"supertest": "^3.0.0"
|
||||
}
|
||||
}
|
21
packages/redux-devtools-cli/src/api/schema.js
Normal file
21
packages/redux-devtools-cli/src/api/schema.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
var makeExecutableSchema = require('graphql-tools').makeExecutableSchema;
|
||||
var requireSchema = require('../utils/requireSchema');
|
||||
var schema = requireSchema('./schema_def.graphql', require);
|
||||
|
||||
var resolvers = {
|
||||
Query: {
|
||||
reports: function report(source, args, context, ast) {
|
||||
return context.store.listAll();
|
||||
},
|
||||
report: function report(source, args, context, ast) {
|
||||
return context.store.get(args.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var executableSchema = makeExecutableSchema({
|
||||
typeDefs: schema,
|
||||
resolvers: resolvers
|
||||
});
|
||||
|
||||
module.exports = executableSchema;
|
60
packages/redux-devtools-cli/src/api/schema_def.graphql
Normal file
60
packages/redux-devtools-cli/src/api/schema_def.graphql
Normal file
|
@ -0,0 +1,60 @@
|
|||
# A list of options for the type of the report
|
||||
enum ReportType {
|
||||
STATE
|
||||
ACTION
|
||||
STATES
|
||||
ACTIONS
|
||||
}
|
||||
|
||||
type Report {
|
||||
# Report ID
|
||||
id: ID!
|
||||
# Type of the report, can be: STATE, ACTION, STATES, ACTIONS
|
||||
type: ReportType,
|
||||
# Briefly what happened
|
||||
title: String,
|
||||
# Details supplied by the user
|
||||
description: String,
|
||||
# The last dispatched action before the report was sent
|
||||
action: String,
|
||||
# Stringified actions or the state or both, which should be loaded the application to reproduce the exact behavior
|
||||
payload: String,
|
||||
# Stringified preloaded state object. Could be the initial state of the app or committed state (after dispatching COMMIT action or reaching maxAge)
|
||||
preloadedState: String,
|
||||
# Screenshot url or blob as a string
|
||||
screenshot: String,
|
||||
# User Agent String
|
||||
userAgent: String,
|
||||
# Application version to group the reports and versioning
|
||||
version: String,
|
||||
# Used to identify the user who sent the report
|
||||
userId: String,
|
||||
# More detailed data about the user, usually it's a stringified object
|
||||
user: String,
|
||||
# Everything else you want to send
|
||||
meta: String,
|
||||
# Error message which invoked sending the report
|
||||
exception: String,
|
||||
# Id to identify the store in case there are multiple stores
|
||||
instanceId: String,
|
||||
# Timestamp when the report was added
|
||||
added: String
|
||||
# Id to identify the application (from apps table)
|
||||
appId: ID
|
||||
}
|
||||
|
||||
# Explore GraphQL query schema
|
||||
type Query {
|
||||
# List all reports
|
||||
reports: [Report]
|
||||
# Get a report by ID
|
||||
report(
|
||||
# Report ID
|
||||
id: ID!
|
||||
): Report
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
#mutation: Mutation
|
||||
}
|
27
packages/redux-devtools-cli/src/db/connector.js
Normal file
27
packages/redux-devtools-cli/src/db/connector.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
var path = require('path');
|
||||
var knexModule = require('knex');
|
||||
|
||||
module.exports = function connector(options) {
|
||||
var dbOptions = options.dbOptions;
|
||||
dbOptions.useNullAsDefault = true;
|
||||
if (!dbOptions.migrate) {
|
||||
return knexModule(dbOptions);
|
||||
}
|
||||
|
||||
dbOptions.migrations = { directory: path.resolve(__dirname, 'migrations') };
|
||||
dbOptions.seeds = { directory: path.resolve(__dirname, 'seeds') };
|
||||
var knex = knexModule(dbOptions);
|
||||
|
||||
knex.migrate.latest()
|
||||
.then(function() {
|
||||
return knex.seed.run();
|
||||
})
|
||||
.then(function() {
|
||||
console.log(' \x1b[0;32m[Done]\x1b[0m Migrations are finished\n');
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
return knex;
|
||||
};
|
71
packages/redux-devtools-cli/src/db/migrations/index.js
Normal file
71
packages/redux-devtools-cli/src/db/migrations/index.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
exports.up = function(knex, Promise) {
|
||||
return Promise.all([
|
||||
knex.schema.createTable('remotedev_reports', function(table) {
|
||||
table.uuid('id').primary();
|
||||
table.string('type');
|
||||
table.string('title');
|
||||
table.string('description');
|
||||
table.string('action');
|
||||
table.text('payload', 'longtext');
|
||||
table.text('preloadedState', 'longtext');
|
||||
table.text('screenshot', 'longtext');
|
||||
table.string('userAgent');
|
||||
table.string('version');
|
||||
table.string('user');
|
||||
table.string('userId');
|
||||
table.string('instanceId');
|
||||
table.string('meta');
|
||||
table.string('exception');
|
||||
table.timestamp('added').defaultTo(knex.fn.now());
|
||||
table.uuid('appId')
|
||||
.references('id')
|
||||
.inTable('remotedev_apps').onDelete('CASCADE').onUpdate('CASCADE')
|
||||
.defaultTo('78626c31-e16b-4528-b8e5-f81301b627f4');
|
||||
}),
|
||||
knex.schema.createTable('remotedev_payloads', function(table){
|
||||
table.uuid('id').primary();
|
||||
table.text('state');
|
||||
table.text('action');
|
||||
table.timestamp('added').defaultTo(knex.fn.now());
|
||||
table.uuid('reportId')
|
||||
.references('id')
|
||||
.inTable('remotedev_reports').onDelete('CASCADE').onUpdate('CASCADE');
|
||||
}),
|
||||
knex.schema.createTable('remotedev_apps', function(table){
|
||||
table.uuid('id').primary();
|
||||
table.string('title');
|
||||
table.string('description');
|
||||
table.string('url');
|
||||
table.timestamps(false, true);
|
||||
}),
|
||||
knex.schema.createTable('remotedev_users', function(table){
|
||||
table.uuid('id').primary();
|
||||
table.string('name');
|
||||
table.string('login');
|
||||
table.string('email');
|
||||
table.string('avatarUrl');
|
||||
table.string('profileUrl');
|
||||
table.string('oauthId');
|
||||
table.string('oauthType');
|
||||
table.string('token');
|
||||
table.timestamps(false, true);
|
||||
}),
|
||||
knex.schema.createTable('remotedev_users_apps', function(table){
|
||||
table.boolean('readOnly').defaultTo(false);
|
||||
table.uuid('userId');
|
||||
table.uuid('appId');
|
||||
table.primary(['userId', 'appId']);
|
||||
table.foreign('userId')
|
||||
.references('id').inTable('remotedev_users').onDelete('CASCADE').onUpdate('CASCADE');
|
||||
table.foreign('appId')
|
||||
.references('id').inTable('remotedev_apps').onDelete('CASCADE').onUpdate('CASCADE');
|
||||
})
|
||||
])
|
||||
};
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return Promise.all([
|
||||
knex.schema.dropTable('remotedev_reports'),
|
||||
knex.schema.dropTable('remotedev_apps')
|
||||
])
|
||||
};
|
12
packages/redux-devtools-cli/src/db/seeds/index.js
Normal file
12
packages/redux-devtools-cli/src/db/seeds/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
exports.seed = function(knex, Promise) {
|
||||
return Promise.all([
|
||||
knex('remotedev_apps').del()
|
||||
]).then(function() {
|
||||
return Promise.all([
|
||||
knex('remotedev_apps').insert({
|
||||
id: '78626c31-e16b-4528-b8e5-f81301b627f4',
|
||||
title: 'Default'
|
||||
})
|
||||
]);
|
||||
});
|
||||
};
|
13
packages/redux-devtools-cli/src/middleware/graphiql.js
Normal file
13
packages/redux-devtools-cli/src/middleware/graphiql.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
var graphiqlExpress = require('graphql-server-express').graphiqlExpress;
|
||||
|
||||
module.exports = graphiqlExpress({
|
||||
endpointURL: '/graphql',
|
||||
query:
|
||||
'{\n' +
|
||||
' reports {\n' +
|
||||
' id,\n' +
|
||||
' type,\n' +
|
||||
' title\n' +
|
||||
' }\n' +
|
||||
'}'
|
||||
});
|
13
packages/redux-devtools-cli/src/middleware/graphql.js
Normal file
13
packages/redux-devtools-cli/src/middleware/graphql.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
var graphqlExpress = require('graphql-server-express').graphqlExpress;
|
||||
var schema = require('../api/schema');
|
||||
|
||||
module.exports = function (store) {
|
||||
return graphqlExpress(function() {
|
||||
return {
|
||||
schema: schema,
|
||||
context: {
|
||||
store: store
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
26
packages/redux-devtools-cli/src/options.js
Normal file
26
packages/redux-devtools-cli/src/options.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
var path = require('path');
|
||||
|
||||
module.exports = function getOptions(argv) {
|
||||
var dbOptions = argv.dbOptions;
|
||||
if (typeof dbOptions === 'string') {
|
||||
dbOptions = require(path.resolve(process.cwd(), argv.dbOptions));
|
||||
} else if (typeof dbOptions === 'undefined') {
|
||||
dbOptions = require('../defaultDbOptions.json');
|
||||
}
|
||||
|
||||
return {
|
||||
host: argv.hostname || process.env.npm_package_remotedev_hostname || null,
|
||||
port: Number(argv.port || process.env.npm_package_remotedev_port) || 8000,
|
||||
protocol: argv.protocol || process.env.npm_package_remotedev_protocol || 'http',
|
||||
protocolOptions: !(argv.protocol === 'https') ? null : {
|
||||
key: argv.key || process.env.npm_package_remotedev_key || null,
|
||||
cert: argv.cert || process.env.npm_package_remotedev_cert || null,
|
||||
passphrase: argv.passphrase || process.env.npm_package_remotedev_passphrase || null
|
||||
},
|
||||
dbOptions: dbOptions,
|
||||
maxRequestBody: argv.passphrase || '16mb',
|
||||
logHTTPRequests: argv.logHTTPRequests,
|
||||
logLevel: argv.logLevel || 3,
|
||||
wsEngine: argv.wsEngine || process.env.npm_package_remotedev_wsengine || 'ws'
|
||||
};
|
||||
}
|
77
packages/redux-devtools-cli/src/routes.js
Normal file
77
packages/redux-devtools-cli/src/routes.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
var path = require('path');
|
||||
var express = require('express');
|
||||
var morgan = require('morgan');
|
||||
var bodyParser = require('body-parser');
|
||||
var cors = require('cors');
|
||||
var graphiqlMiddleware = require('./middleware/graphiql');
|
||||
var graphqlMiddleware = require('./middleware/graphql');
|
||||
|
||||
var app = express.Router();
|
||||
|
||||
function serveUmdModule(name) {
|
||||
app.use(express.static(require.resolve(name).match(/.*\/(node_modules|packages)\/[^/]+\//)[0] + 'umd'));
|
||||
}
|
||||
|
||||
function routes(options, store) {
|
||||
var limit = options.maxRequestBody;
|
||||
var logHTTPRequests = options.logHTTPRequests;
|
||||
|
||||
if (logHTTPRequests) {
|
||||
if (typeof logHTTPRequests === 'object') app.use(morgan('combined', logHTTPRequests));
|
||||
else app.use(morgan('combined'));
|
||||
}
|
||||
|
||||
app.use('/graphiql', graphiqlMiddleware);
|
||||
|
||||
serveUmdModule('react');
|
||||
serveUmdModule('react-dom');
|
||||
serveUmdModule('redux-devtools-core');
|
||||
|
||||
app.get('/port.js', function (req, res) {
|
||||
res.send('reduxDevToolsPort = ' + options.port);
|
||||
});
|
||||
app.get('*', function (req, res) {
|
||||
res.sendFile(path.join(__dirname, '../app/index.html'));
|
||||
});
|
||||
|
||||
app.use(cors({methods: 'POST'}));
|
||||
app.use(bodyParser.json({limit: limit}));
|
||||
app.use(bodyParser.urlencoded({limit: limit, extended: false}));
|
||||
|
||||
app.use('/graphql', graphqlMiddleware(store));
|
||||
|
||||
app.post('/', function (req, res) {
|
||||
if (!req.body) return res.status(404).end();
|
||||
switch (req.body.op) {
|
||||
case 'get':
|
||||
store.get(req.body.id).then(function (r) {
|
||||
res.send(r || {});
|
||||
}).catch(function (error) {
|
||||
console.error(error);
|
||||
res.sendStatus(500)
|
||||
});
|
||||
break;
|
||||
case 'list':
|
||||
store.list(req.body.query, req.body.fields).then(function (r) {
|
||||
res.send(r);
|
||||
}).catch(function (error) {
|
||||
console.error(error);
|
||||
res.sendStatus(500)
|
||||
});
|
||||
break;
|
||||
default:
|
||||
store.add(req.body).then(function (r) {
|
||||
res.send({id: r.id, error: r.error});
|
||||
scServer.exchange.publish('report', {
|
||||
type: 'add', data: r
|
||||
});
|
||||
}).catch(function (error) {
|
||||
console.error(error);
|
||||
res.status(500).send({})
|
||||
});
|
||||
}
|
||||
});
|
||||
return app;
|
||||
}
|
||||
|
||||
module.exports = routes;
|
103
packages/redux-devtools-cli/src/store.js
Normal file
103
packages/redux-devtools-cli/src/store.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
var uuidV4 = require('uuid/v4');
|
||||
var pick = require('lodash/pick');
|
||||
var connector = require('./db/connector');
|
||||
|
||||
var reports = 'remotedev_reports';
|
||||
// var payloads = 'remotedev_payloads';
|
||||
var knex;
|
||||
|
||||
var baseFields = ['id', 'title', 'added'];
|
||||
|
||||
function error(msg) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
return resolve({ error: msg });
|
||||
});
|
||||
}
|
||||
|
||||
function list(query, fields) {
|
||||
var r = knex.select(fields || baseFields).from(reports);
|
||||
if (query) return r.where(query);
|
||||
return r;
|
||||
}
|
||||
|
||||
function listAll(query) {
|
||||
var r = knex.select().from(reports);
|
||||
if (query) return r.where(query);
|
||||
return r;
|
||||
}
|
||||
|
||||
function get(id) {
|
||||
if (!id) return error('No id specified.');
|
||||
|
||||
return knex(reports).where('id', id).first();
|
||||
}
|
||||
|
||||
function add(data) {
|
||||
if (!data.type || !data.payload) {
|
||||
return error('Required parameters aren\'t specified.');
|
||||
}
|
||||
if (data.type !== 'ACTIONS' && data.type !== 'STATE') {
|
||||
return error('Type ' + data.type + ' is not supported yet.');
|
||||
}
|
||||
|
||||
var reportId = uuidV4();
|
||||
var report = {
|
||||
id: reportId,
|
||||
type: data.type,
|
||||
title: data.title || data.exception && data.exception.message || data.action,
|
||||
description: data.description,
|
||||
action: data.action,
|
||||
payload: data.payload,
|
||||
preloadedState: data.preloadedState,
|
||||
screenshot: data.screenshot,
|
||||
version: data.version,
|
||||
userAgent: data.userAgent,
|
||||
user: data.user,
|
||||
userId: typeof data.user === 'object' ? data.user.id : data.user,
|
||||
instanceId: data.instanceId,
|
||||
meta: data.meta,
|
||||
exception: composeException(data.exception),
|
||||
added: new Date().toISOString(),
|
||||
};
|
||||
if (data.appId) report.appId = data.appId; // TODO check if the id exists and we have access to link it
|
||||
/*
|
||||
var payload = {
|
||||
id: uuid.v4(),
|
||||
reportId: reportId,
|
||||
state: data.payload
|
||||
};
|
||||
*/
|
||||
|
||||
return knex.insert(report).into(reports)
|
||||
.then(function (){ return byBaseFields(report); })
|
||||
}
|
||||
|
||||
function byBaseFields(data) {
|
||||
return pick(data, baseFields);
|
||||
}
|
||||
|
||||
function createStore(options) {
|
||||
knex = connector(options);
|
||||
|
||||
return {
|
||||
list: list,
|
||||
listAll: listAll,
|
||||
get: get,
|
||||
add: add
|
||||
};
|
||||
}
|
||||
|
||||
function composeException(exception) {
|
||||
var message = '';
|
||||
|
||||
if (exception) {
|
||||
message = 'Exception thrown: ';
|
||||
if (exception.message)
|
||||
message += exception.message;
|
||||
if (exception.stack)
|
||||
message += '\n' + exception.stack;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
module.exports = createStore;
|
6
packages/redux-devtools-cli/src/utils/requireSchema.js
Normal file
6
packages/redux-devtools-cli/src/utils/requireSchema.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
var fs = require('fs');
|
||||
|
||||
module.exports = function(name, require) {
|
||||
return fs.readFileSync(require.resolve(name)).toString();
|
||||
// return GraphQL.buildSchema(schema);
|
||||
};
|
75
packages/redux-devtools-cli/src/worker.js
Normal file
75
packages/redux-devtools-cli/src/worker.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
var SCWorker = require("socketcluster/scworker");
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var routes = require('./routes');
|
||||
var createStore = require('./store');
|
||||
|
||||
class Worker extends SCWorker {
|
||||
run() {
|
||||
var httpServer = this.httpServer;
|
||||
var scServer = this.scServer;
|
||||
var options = this.options;
|
||||
var store = createStore(options);
|
||||
|
||||
httpServer.on('request', app);
|
||||
|
||||
app.use(routes(options, store));
|
||||
|
||||
scServer.addMiddleware(scServer.MIDDLEWARE_EMIT, function (req, next) {
|
||||
var channel = req.event;
|
||||
var data = req.data;
|
||||
if (channel.substr(0, 3) === 'sc-' || channel === 'respond' || channel === 'log') {
|
||||
scServer.exchange.publish(channel, data);
|
||||
} else if (channel === 'log-noid') {
|
||||
scServer.exchange.publish('log', {id: req.socket.id, data: data});
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
scServer.addMiddleware(scServer.MIDDLEWARE_SUBSCRIBE, function (req, next) {
|
||||
next();
|
||||
if (req.channel === 'report') {
|
||||
store.list().then(function (data) {
|
||||
req.socket.emit(req.channel, {type: 'list', data: data});
|
||||
}).catch(function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
scServer.on('connection', function (socket) {
|
||||
var channelToWatch, channelToEmit;
|
||||
socket.on('login', function (credentials, respond) {
|
||||
if (credentials === 'master') {
|
||||
channelToWatch = 'respond';
|
||||
channelToEmit = 'log';
|
||||
} else {
|
||||
channelToWatch = 'log';
|
||||
channelToEmit = 'respond';
|
||||
}
|
||||
this.exchange.subscribe('sc-' + socket.id).watch(function (msg) {
|
||||
socket.emit(channelToWatch, msg);
|
||||
});
|
||||
respond(null, channelToWatch);
|
||||
});
|
||||
socket.on('getReport', function (id, respond) {
|
||||
store.get(id).then(function (data) {
|
||||
respond(null, data);
|
||||
}).catch(function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
socket.on('disconnect', function () {
|
||||
var channel = this.exchange.channel('sc-' + socket.id);
|
||||
channel.unsubscribe();
|
||||
channel.destroy();
|
||||
scServer.exchange.publish(
|
||||
channelToEmit,
|
||||
{id: socket.id, type: 'DISCONNECTED'}
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
new Worker();
|
185
packages/redux-devtools-cli/test/integration.spec.js
Normal file
185
packages/redux-devtools-cli/test/integration.spec.js
Normal file
|
@ -0,0 +1,185 @@
|
|||
var childProcess = require('child_process');
|
||||
var request = require('supertest');
|
||||
var expect = require('expect');
|
||||
var scClient = require('socketcluster-client');
|
||||
|
||||
describe('Server', function() {
|
||||
var scServer;
|
||||
this.timeout(5000);
|
||||
before(function(done) {
|
||||
scServer = childProcess.fork(__dirname + '/../bin/redux-devtools.js');
|
||||
setTimeout(done, 2000);
|
||||
});
|
||||
|
||||
after(function() {
|
||||
if (scServer) {
|
||||
scServer.kill();
|
||||
}
|
||||
});
|
||||
|
||||
describe('Express backend', function() {
|
||||
it('loads main page', function() {
|
||||
request('http://localhost:8000')
|
||||
.get('/')
|
||||
.expect('Content-Type', /text\/html/)
|
||||
.expect(200)
|
||||
.then(function(res) {
|
||||
expect(res.text).toMatch(/<title>Redux DevTools<\/title>/);
|
||||
})
|
||||
});
|
||||
|
||||
it('resolves an inexistent url', function(done) {
|
||||
request('http://localhost:8000/jreerfr/123')
|
||||
.get('/')
|
||||
.expect('Content-Type', /text\/html/)
|
||||
.expect(200, done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Realtime monitoring', function() {
|
||||
var socket, socket2, channel;
|
||||
before(function() {
|
||||
socket = scClient.connect({ hostname: 'localhost', port: 8000 });
|
||||
socket.connect();
|
||||
socket.on('error', function(error) {
|
||||
console.error('Socket1 error', error);
|
||||
});
|
||||
socket2 = scClient.connect({ hostname: 'localhost', port: 8000 });
|
||||
socket2.connect();
|
||||
socket.on('error', function(error) {
|
||||
console.error('Socket2 error', error);
|
||||
});
|
||||
});
|
||||
|
||||
after(function() {
|
||||
socket.disconnect();
|
||||
socket2.disconnect();
|
||||
});
|
||||
|
||||
it('should connect', function(done) {
|
||||
socket.on('connect', function(status) {
|
||||
expect(status.id).toExist();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should login', function() {
|
||||
socket.emit('login', 'master', function(error, channelName) {
|
||||
if (error) { console.log(error); return; }
|
||||
expect(channelName).toBe('respond');
|
||||
channel = socket.subscribe(channelName);
|
||||
expect(channel.SUBSCRIBED).toBe('subscribed');
|
||||
});
|
||||
});
|
||||
|
||||
it('should send message', function(done) {
|
||||
var data = {
|
||||
"type": "ACTION",
|
||||
"payload": {
|
||||
"todos": "do some"
|
||||
},
|
||||
"action": {
|
||||
"timestamp": 1483349708506,
|
||||
"action": {
|
||||
"type": "ADD_TODO",
|
||||
"text": "hggg"
|
||||
}
|
||||
},
|
||||
"instanceId": "tAmA7H5fclyWhvizAAAi",
|
||||
"name": "LoggerInstance",
|
||||
"id": "tAmA7H5fclyWhvizAAAi"
|
||||
};
|
||||
|
||||
socket2.emit('login', '', function(error, channelName) {
|
||||
if (error) { console.log(error); return; }
|
||||
expect(channelName).toBe('log');
|
||||
var channel2 = socket2.subscribe(channelName);
|
||||
expect(channel2.SUBSCRIBED).toBe('subscribed');
|
||||
channel2.on('subscribe', function() {
|
||||
channel2.watch(function(message) {
|
||||
expect(message).toEqual(data);
|
||||
done();
|
||||
});
|
||||
socket.emit(channelName, data);
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('REST backend', function() {
|
||||
var id;
|
||||
var report = {
|
||||
type: 'ACTIONS',
|
||||
title: 'Test report',
|
||||
description: 'Test body report',
|
||||
action: 'SOME_FINAL_ACTION',
|
||||
payload: '[{"type":"ADD_TODO","text":"hi"},{"type":"SOME_FINAL_ACTION"}]',
|
||||
preloadedState: '{"todos":[{"text":"Use Redux","completed":false,"id":0}]}',
|
||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36'
|
||||
};
|
||||
it('should add a report', function() {
|
||||
request('http://localhost:8000')
|
||||
.post('/')
|
||||
.send(report)
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /application\/json/)
|
||||
.expect(200)
|
||||
.then(function(res) {
|
||||
id = res.body.id;
|
||||
expect(id).toExist();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the report', function() {
|
||||
request('http://localhost:8000')
|
||||
.post('/')
|
||||
.send({
|
||||
op: 'get',
|
||||
id: id
|
||||
})
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /application\/json/)
|
||||
.expect(200)
|
||||
.then(function(res) {
|
||||
expect(res.body).toInclude(report);
|
||||
});
|
||||
});
|
||||
|
||||
it('should list reports', function() {
|
||||
request('http://localhost:8000')
|
||||
.post('/')
|
||||
.send({
|
||||
op: 'list'
|
||||
})
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /application\/json/)
|
||||
.expect(200)
|
||||
.then(function(res) {
|
||||
expect(res.body.length).toBe(1);
|
||||
expect(res.body[0].id).toBe(id);
|
||||
expect(res.body[0].title).toBe('Test report');
|
||||
expect(res.body[0].added).toExist();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GraphQL backend', function() {
|
||||
it('should get the report', function() {
|
||||
request('http://localhost:8000')
|
||||
.post('/graphql')
|
||||
.send({
|
||||
query: '{ reports { id, type, title } }'
|
||||
})
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /application\/json/)
|
||||
.expect(200)
|
||||
.then(function(res) {
|
||||
var reports = res.body.data.reports;
|
||||
expect(reports.length).toBe(1);
|
||||
expect(reports[0].id).toExist();
|
||||
expect(reports[0].title).toBe('Test report');
|
||||
expect(reports[0].type).toBe('ACTIONS');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"presets": ["es2015-loose", "stage-0", "react"]
|
||||
"presets": [ "es2015", "stage-0", "react" ],
|
||||
"plugins": [ "add-module-exports", "transform-decorators-legacy" ]
|
||||
}
|
||||
|
|
|
@ -1,16 +1,26 @@
|
|||
{
|
||||
"extends": "eslint-config-airbnb",
|
||||
"globals": {
|
||||
"chrome": true
|
||||
},
|
||||
"env": {
|
||||
"mocha": true,
|
||||
"jest": true,
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"rules": {
|
||||
"react/prefer-stateless-function": 0,
|
||||
"react/no-array-index-key": 0,
|
||||
"react/forbid-prop-types": 0,
|
||||
"react/require-default-props": 0,
|
||||
"react/jsx-filename-extension": 0,
|
||||
"react/jsx-uses-react": 2,
|
||||
"react/jsx-uses-vars": 2,
|
||||
"react/react-in-jsx-scope": 2,
|
||||
"react/sort-comp": 0,
|
||||
"react/jsx-quotes": 0,
|
||||
"import/no-extraneous-dependencies": 0,
|
||||
"block-scoped-var": 0,
|
||||
"padded-blocks": 0,
|
||||
"quotes": [ 1, "single" ],
|
||||
|
@ -20,16 +30,22 @@
|
|||
"no-console": 0,
|
||||
"func-names": 0,
|
||||
"prefer-const": 0,
|
||||
"prefer-arrow-callback": 0,
|
||||
"comma-dangle": 0,
|
||||
"id-length": 0,
|
||||
"no-use-before-define": 0,
|
||||
"indent": [2, 2, {"SwitchCase": 1}],
|
||||
"default-case": 0,
|
||||
"prefer-template": 0,
|
||||
"prefer-rest-params": 0,
|
||||
"no-proto": 0,
|
||||
"new-cap": [2, { "capIsNewExceptions": ["Test"] }],
|
||||
"no-underscore-dangle": 0,
|
||||
"max-len": ["error", { "code": 120 }]
|
||||
"no-plusplus": 0,
|
||||
"no-proto": 0,
|
||||
"arrow-parens": 0,
|
||||
"prefer-arrow-callback": 0,
|
||||
"prefer-rest-params": 0,
|
||||
"prefer-template": 0,
|
||||
"class-methods-use-this": 0,
|
||||
"max-len": ["error", { "code": 120 }],
|
||||
"no-mixed-operators": 0,
|
||||
"no-undef": 0
|
||||
},
|
||||
"plugins": [
|
||||
"react"
|
||||
|
|
45
packages/redux-devtools-core/README.md
Executable file → Normal file
45
packages/redux-devtools-core/README.md
Executable file → Normal file
|
@ -1,5 +1,44 @@
|
|||
Common functions for [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension) and [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools).
|
||||
Redux DevTools monitor app
|
||||
==================================
|
||||
|
||||
## LICENSE
|
||||
![Demo](https://raw.githubusercontent.com/zalmoxisus/remote-redux-devtools/master/demo.gif)
|
||||
|
||||
[MIT](LICENSE)
|
||||
Web, Electron and Chrome app for monitoring [remote-redux-devtools](https://github.com/zalmoxisus/remote-redux-devtools). Can be accessed on [`remotedev.io`](http://remotedev.io/local).
|
||||
|
||||
Also it's a react component you can use to build amazing monitor applications like:
|
||||
|
||||
* [redux-devtools-extension](https://github.com/zalmoxisus/redux-devtools-extension).
|
||||
* [react-native-debugger](https://github.com/jhen0409/react-native-debugger) - Electron app, which already includes `remotedev-server`, `redux-devtools-core` and even React DevTools.
|
||||
* [remote-redux-devtools-on-debugger](https://github.com/jhen0409/remote-redux-devtools-on-debugger) - Used in React Native debugger as a dock monitor.
|
||||
* [atom-redux-devtools](https://github.com/zalmoxisus/atom-redux-devtools) - Used in Atom editor.
|
||||
* [vscode-redux-devtools](https://github.com/jkzing/vscode-redux-devtools) - Used in Visual Studio Code.
|
||||
|
||||
### Usage
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import ReactDom from 'react-dom';
|
||||
import DevToolsApp from 'redux-devtools-core/lib/app';
|
||||
|
||||
ReactDom.render(
|
||||
<App />,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
* `socketOptions` - *object* used to specify predefined options for the connection:
|
||||
* `hostname` - *string*
|
||||
* `port` - *number or string*
|
||||
* `autoReconnect` - *boolean*
|
||||
* `secure` - *boolean*.
|
||||
* `monitorOptions` - *object* used to specify predefined monitor options:
|
||||
* `selected` - *string* - which monitor is selected by default. One of the following values: `LogMonitor`, `InspectorMonitor`, `ChartMonitor`.
|
||||
* `testTemplates` - *array* of strings representing predefined test templates.
|
||||
* `noSettings` - *boolean* set to `true` in order to hide settings button and dialog.
|
||||
|
||||
### License
|
||||
|
||||
MIT
|
||||
|
|
45
packages/redux-devtools-core/assets/index.html
Normal file
45
packages/redux-devtools-core/assets/index.html
Normal file
|
@ -0,0 +1,45 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
||||
<title>Redux DevTools</title>
|
||||
<style>
|
||||
body {
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media print {
|
||||
@page {
|
||||
size: auto;
|
||||
margin: 0;
|
||||
}
|
||||
body {
|
||||
position: static;
|
||||
}
|
||||
#root > div > div:not(:nth-child(2)) {
|
||||
display: none !important;
|
||||
}
|
||||
#root > div > div:nth-child(2) {
|
||||
overflow: visible !important;
|
||||
position: absolute !important;
|
||||
z-index: 2147483647;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
#root > div > div:nth-child(2) * {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id='root'></div>
|
||||
</body>
|
||||
</html>
|
25
packages/redux-devtools-core/index.js
Normal file
25
packages/redux-devtools-core/index.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import App from './src/app';
|
||||
|
||||
render(
|
||||
<App />,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
if (module.hot) {
|
||||
// https://github.com/webpack/webpack/issues/418#issuecomment-53398056
|
||||
module.hot.accept(err => {
|
||||
if (err) console.error(err.message);
|
||||
});
|
||||
|
||||
/*
|
||||
module.hot.accept('./app', () => {
|
||||
const NextApp = require('./app').default;
|
||||
render(
|
||||
<NextApp />,
|
||||
document.getElementById('root')
|
||||
);
|
||||
});
|
||||
*/
|
||||
}
|
|
@ -1,20 +1,31 @@
|
|||
{
|
||||
"name": "redux-devtools-core",
|
||||
"version": "0.2.1",
|
||||
"version": "1.0.0-1",
|
||||
"description": "Reusable functions of Redux DevTools",
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --hot --inline --env.development --env.platform=web --progress",
|
||||
"build:web": "rimraf ./build/web && webpack -p --env.platform=web --progress",
|
||||
"build:umd": "rimraf ./umd && webpack --progress --config webpack.config.umd.js",
|
||||
"build:umd:min": "webpack --env.minimize --progress --config webpack.config.umd.js",
|
||||
"build": "rimraf ./lib && babel ./src/app --out-dir lib",
|
||||
"clean": "rimraf lib",
|
||||
"lint": "eslint src test",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"test": "NODE_ENV=test jest --no-cache",
|
||||
"prepare": "npm run build && npm run build:umd && npm run build:umd:min",
|
||||
"prepublishOnly": "eslint ./src/app && npm run test && npm run build && npm run build:umd && npm run build:umd:min"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
"lib"
|
||||
"lib",
|
||||
"umd"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf lib",
|
||||
"build:lib": "babel src --out-dir lib",
|
||||
"build": "npm run build:lib",
|
||||
"lint": "eslint src test",
|
||||
"lintfix": "eslint src --fix",
|
||||
"prepare": "npm run build",
|
||||
"prepublishOnly": "npm run lint && npm run clean && npm run build"
|
||||
"jest": {
|
||||
"setupTestFrameworkScriptFile": "<rootDir>/test/setup.js",
|
||||
"moduleNameMapper": {
|
||||
"\\.(css|scss)$": "<rootDir>/test/__mocks__/styleMock.js"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -29,28 +40,71 @@
|
|||
"devDependencies": {
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^6.1.2",
|
||||
"babel-loader": "^6.2.4",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-loader": "^7.1.5",
|
||||
"babel-plugin-add-module-exports": "^0.2.1",
|
||||
"babel-plugin-react-transform": "^2.0.2",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-preset-env": "^1.1.8",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-es2015-loose": "^7.0.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"eslint": "^3.2.0",
|
||||
"eslint-config-airbnb": "^9.0.1",
|
||||
"eslint-plugin-import": "^1.12.0",
|
||||
"eslint-plugin-jsx-a11y": "^2.0.1",
|
||||
"eslint-plugin-react": "^5.2.2",
|
||||
"rimraf": "^2.5.2"
|
||||
"babel-preset-react": "^6.22.0",
|
||||
"babel-preset-stage-0": "^6.22.0",
|
||||
"babel-register": "^6.22.0",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"css-loader": "^2.1.0",
|
||||
"enzyme": "^3.1.0",
|
||||
"enzyme-adapter-react-16": "^1.0.2",
|
||||
"enzyme-to-json": "^3.1.4",
|
||||
"eslint": "^3.15.0",
|
||||
"eslint-config-airbnb": "^14.1.0",
|
||||
"eslint-plugin-import": "^2.2.0",
|
||||
"eslint-plugin-jsx-a11y": "^4.0.0",
|
||||
"eslint-plugin-react": "^6.9.0",
|
||||
"file-loader": "^3.0.0",
|
||||
"html-loader": "^0.4.4",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"jest": "^21.2.1",
|
||||
"raw-loader": "^1.0.0",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"redux-logger": "^2.2.1",
|
||||
"rimraf": "^2.5.4",
|
||||
"style-loader": "^0.13.0",
|
||||
"terser-webpack-plugin": "^1.2.1",
|
||||
"url-loader": "^1.1.0",
|
||||
"webpack": "^4.27.1",
|
||||
"webpack-cli": "^3.2.0",
|
||||
"webpack-dev-server": "^3.1.14",
|
||||
"webpack-hot-middleware": "^2.16.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"d3-state-visualizer": "^1.3.1",
|
||||
"devui": "^1.0.0-2",
|
||||
"get-params": "^0.1.2",
|
||||
"javascript-stringify": "^1.5.0",
|
||||
"jsan": "^3.1.13",
|
||||
"lodash": "^4.17.11",
|
||||
"jsondiffpatch": "^0.2.4",
|
||||
"localforage": "^1.5.0",
|
||||
"lodash": "^4.0.0",
|
||||
"nanoid": "^2.0.0",
|
||||
"remotedev-serialize": "^0.1.8"
|
||||
"prop-types": "^15.5.10",
|
||||
"react-icons": "^2.2.5",
|
||||
"react-redux": "^5.0.5",
|
||||
"redux": "^3.0.5",
|
||||
"redux-devtools": "^3.4.0",
|
||||
"redux-devtools-chart-monitor": "^1.6.1",
|
||||
"redux-devtools-inspector": "^0.11.0",
|
||||
"redux-devtools-instrument": "^1.8.0",
|
||||
"redux-devtools-log-monitor": "^1.3.0",
|
||||
"redux-devtools-test-generator": "^0.5.1",
|
||||
"redux-devtools-trace-monitor": "^0.1.0",
|
||||
"redux-persist": "^4.8.0",
|
||||
"redux-slider-monitor": "^2.0.0-0",
|
||||
"remotedev-serialize": "^0.1.8",
|
||||
"socketcluster-client": "^5.5.0",
|
||||
"styled-components": "^2.0.0"
|
||||
},
|
||||
"pre-commit": [
|
||||
"lint"
|
||||
]
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0"
|
||||
}
|
||||
}
|
||||
|
|
110
packages/redux-devtools-core/src/app/actions/index.js
Normal file
110
packages/redux-devtools-core/src/app/actions/index.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
import {
|
||||
CHANGE_SECTION, CHANGE_THEME, SELECT_INSTANCE, SELECT_MONITOR, UPDATE_MONITOR_STATE,
|
||||
LIFTED_ACTION, MONITOR_ACTION, EXPORT, TOGGLE_SYNC, TOGGLE_SLIDER, TOGGLE_DISPATCHER,
|
||||
TOGGLE_PERSIST, GET_REPORT_REQUEST, SHOW_NOTIFICATION, CLEAR_NOTIFICATION
|
||||
} from '../constants/actionTypes';
|
||||
import { RECONNECT } from '../constants/socketActionTypes';
|
||||
|
||||
let monitorReducer;
|
||||
let monitorProps = {};
|
||||
|
||||
export function changeSection(section) {
|
||||
return { type: CHANGE_SECTION, section };
|
||||
}
|
||||
|
||||
export function changeTheme(data) {
|
||||
return { type: CHANGE_THEME, ...data.formData };
|
||||
}
|
||||
|
||||
export function liftedDispatch(action) {
|
||||
if (action.type[0] === '@') {
|
||||
if (action.type === '@@INIT_MONITOR') {
|
||||
monitorReducer = action.update;
|
||||
monitorProps = action.monitorProps;
|
||||
}
|
||||
return { type: MONITOR_ACTION, action, monitorReducer, monitorProps };
|
||||
}
|
||||
return { type: LIFTED_ACTION, message: 'DISPATCH', action };
|
||||
}
|
||||
|
||||
export function selectInstance(selected) {
|
||||
return { type: SELECT_INSTANCE, selected };
|
||||
}
|
||||
|
||||
export function selectMonitor(monitor) {
|
||||
return { type: SELECT_MONITOR, monitor };
|
||||
}
|
||||
|
||||
export function selectMonitorWithState(value, monitorState) {
|
||||
return { type: SELECT_MONITOR, monitor: value, monitorState };
|
||||
}
|
||||
|
||||
export function selectMonitorTab(subTabName) {
|
||||
return { type: UPDATE_MONITOR_STATE, nextState: { subTabName } };
|
||||
}
|
||||
|
||||
export function updateMonitorState(nextState) {
|
||||
return { type: UPDATE_MONITOR_STATE, nextState };
|
||||
}
|
||||
|
||||
export function importState(state, preloadedState) {
|
||||
return { type: LIFTED_ACTION, message: 'IMPORT', state, preloadedState };
|
||||
}
|
||||
|
||||
export function exportState() {
|
||||
return { type: EXPORT };
|
||||
}
|
||||
|
||||
export function lockChanges(status) {
|
||||
return {
|
||||
type: LIFTED_ACTION,
|
||||
message: 'DISPATCH',
|
||||
action: { type: 'LOCK_CHANGES', status },
|
||||
toAll: true
|
||||
};
|
||||
}
|
||||
|
||||
export function pauseRecording(status) {
|
||||
return {
|
||||
type: LIFTED_ACTION,
|
||||
message: 'DISPATCH',
|
||||
action: { type: 'PAUSE_RECORDING', status },
|
||||
toAll: true
|
||||
};
|
||||
}
|
||||
|
||||
export function dispatchRemotely(action) {
|
||||
return { type: LIFTED_ACTION, message: 'ACTION', action };
|
||||
}
|
||||
|
||||
export function togglePersist() {
|
||||
return { type: TOGGLE_PERSIST };
|
||||
}
|
||||
|
||||
export function toggleSync() {
|
||||
return { type: TOGGLE_SYNC };
|
||||
}
|
||||
|
||||
export function toggleSlider() {
|
||||
return { type: TOGGLE_SLIDER };
|
||||
}
|
||||
|
||||
export function toggleDispatcher() {
|
||||
return { type: TOGGLE_DISPATCHER };
|
||||
}
|
||||
|
||||
export function saveSocketSettings(options) {
|
||||
return { type: RECONNECT, options };
|
||||
}
|
||||
|
||||
export function showNotification(message) {
|
||||
return { type: SHOW_NOTIFICATION, notification: { type: 'error', message } };
|
||||
}
|
||||
|
||||
export function clearNotification() {
|
||||
return { type: CLEAR_NOTIFICATION };
|
||||
}
|
||||
|
||||
export function getReport(report) {
|
||||
return { type: GET_REPORT_REQUEST, report };
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Toolbar, Divider, Spacer } from 'devui';
|
||||
import SaveIcon from 'react-icons/lib/md/save';
|
||||
import ExportButton from './buttons/ExportButton';
|
||||
import ImportButton from './buttons/ImportButton';
|
||||
import PrintButton from './buttons/PrintButton';
|
||||
import DispatcherButton from './buttons/DispatcherButton';
|
||||
import SliderButton from './buttons/SliderButton';
|
||||
import MonitorSelector from './MonitorSelector';
|
||||
|
||||
export default class BottomButtons extends Component {
|
||||
static propTypes = {
|
||||
dispatcherIsOpen: PropTypes.bool,
|
||||
sliderIsOpen: PropTypes.bool,
|
||||
options: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextProps.dispatcherIsOpen !== this.props.dispatcherIsOpen
|
||||
|| nextProps.sliderIsOpen !== this.props.sliderIsOpen
|
||||
|| nextProps.options !== this.props.options;
|
||||
}
|
||||
|
||||
render() {
|
||||
const features = this.props.options.features;
|
||||
return (
|
||||
<Toolbar borderPosition="top">
|
||||
{features.export &&
|
||||
<Button
|
||||
title="Save a report"
|
||||
tooltipPosition="top-right"
|
||||
>
|
||||
<SaveIcon />
|
||||
</Button>
|
||||
}
|
||||
{features.export &&
|
||||
<ExportButton />
|
||||
}
|
||||
{features.import &&
|
||||
<ImportButton />
|
||||
}
|
||||
<PrintButton />
|
||||
<Divider />
|
||||
<MonitorSelector />
|
||||
<Divider />
|
||||
{features.jump &&
|
||||
<SliderButton isOpen={this.props.sliderIsOpen} />
|
||||
}
|
||||
{features.dispatch &&
|
||||
<DispatcherButton dispatcherIsOpen={this.props.dispatcherIsOpen} />
|
||||
}
|
||||
</Toolbar>
|
||||
);
|
||||
}
|
||||
}
|
78
packages/redux-devtools-core/src/app/components/Header.js
Normal file
78
packages/redux-devtools-core/src/app/components/Header.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Tabs, Toolbar, Button, Divider, Spacer } from 'devui';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import DocsIcon from 'react-icons/lib/go/book';
|
||||
import FeedBackIcon from 'react-icons/lib/io/android-textsms';
|
||||
import TwitterIcon from 'react-icons/lib/ti/social-twitter';
|
||||
import SupportIcon from 'react-icons/lib/ti/heart-full-outline';
|
||||
import { changeSection } from '../actions';
|
||||
|
||||
const tabs = [
|
||||
{ name: 'Actions' },
|
||||
{ name: 'Reports' },
|
||||
{ name: 'Settings' }
|
||||
];
|
||||
|
||||
class Header extends Component {
|
||||
static propTypes = {
|
||||
section: PropTypes.string.isRequired,
|
||||
changeSection: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
openLink = url => () => {
|
||||
window.open(url);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Toolbar compact noBorder borderPosition="bottom">
|
||||
<Tabs
|
||||
main
|
||||
collapsible
|
||||
tabs={tabs}
|
||||
onClick={this.props.changeSection}
|
||||
selected={this.props.section || 'Actions'}
|
||||
/>
|
||||
<Divider />
|
||||
<Button
|
||||
title="Documentation"
|
||||
tooltipPosition="bottom"
|
||||
onClick={this.openLink('http://extension.remotedev.io')}
|
||||
>
|
||||
<DocsIcon />
|
||||
</Button>
|
||||
<Button
|
||||
title="Feedback"
|
||||
tooltipPosition="bottom"
|
||||
onClick={this.openLink('http://extension.remotedev.io/docs/Feedback.html')}
|
||||
>
|
||||
<FeedBackIcon />
|
||||
</Button>
|
||||
<Button
|
||||
title="Follow us"
|
||||
tooltipPosition="bottom"
|
||||
onClick={this.openLink('https://twitter.com/ReduxDevTools')}
|
||||
>
|
||||
<TwitterIcon />
|
||||
</Button>
|
||||
<Button
|
||||
title="Support us"
|
||||
tooltipPosition="bottom-left"
|
||||
onClick={this.openLink('https://opencollective.com/redux-devtools-extension')}
|
||||
>
|
||||
<SupportIcon />
|
||||
</Button>
|
||||
</Toolbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
changeSection: bindActionCreators(changeSection, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(Header);
|
|
@ -0,0 +1,47 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Select } from 'devui';
|
||||
import { selectInstance } from '../actions';
|
||||
|
||||
class InstanceSelector extends Component {
|
||||
static propTypes = {
|
||||
selected: PropTypes.string,
|
||||
instances: PropTypes.object.isRequired,
|
||||
onSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
this.select = [{ value: '', label: 'Autoselect instances' }];
|
||||
const instances = this.props.instances;
|
||||
let name;
|
||||
Object.keys(instances).forEach(key => {
|
||||
name = instances[key].name;
|
||||
if (name !== undefined) this.select.push({ value: key, label: instances[key].name });
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={this.select}
|
||||
onChange={this.props.onSelect}
|
||||
value={this.props.selected || ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
selected: state.instances.selected,
|
||||
instances: state.instances.options
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onSelect: bindActionCreators(selectInstance, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(InstanceSelector);
|
|
@ -0,0 +1,45 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Tabs } from 'devui';
|
||||
import { monitors } from '../utils/getMonitor';
|
||||
import { selectMonitor } from '../actions';
|
||||
|
||||
class MonitorSelector extends Component {
|
||||
static propTypes = {
|
||||
selected: PropTypes.string,
|
||||
selectMonitor: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.selected !== this.props.selected;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Tabs
|
||||
main
|
||||
collapsible
|
||||
position="center"
|
||||
tabs={monitors}
|
||||
onClick={this.props.selectMonitor}
|
||||
selected={this.props.selected || 'InspectorMonitor'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
selected: state.monitor.selected
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
selectMonitor: bindActionCreators(selectMonitor, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(MonitorSelector);
|
|
@ -0,0 +1,126 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Container, Form, Button } from 'devui';
|
||||
import { saveSocketSettings } from '../../actions';
|
||||
|
||||
const defaultSchema = {
|
||||
type: 'object',
|
||||
required: [],
|
||||
properties: {
|
||||
type: {
|
||||
title: 'Connection settings (for getting reports and remote debugging)',
|
||||
type: 'string',
|
||||
enum: ['disabled', 'remotedev', 'custom'],
|
||||
enumNames: ['no remote connection', 'connect via remotedev.io', 'use local (custom) server']
|
||||
},
|
||||
hostname: {
|
||||
type: 'string'
|
||||
},
|
||||
port: {
|
||||
type: 'number'
|
||||
},
|
||||
secure: {
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const uiSchema = {
|
||||
type: {
|
||||
'ui:widget': 'radio'
|
||||
}
|
||||
};
|
||||
|
||||
class Connection extends Component {
|
||||
static propTypes = {
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
options: PropTypes.object.isRequired,
|
||||
type: PropTypes.string
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = this.setFormData(props.type);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return this.state !== nextState;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.options !== nextProps.options) {
|
||||
this.setState({ formData: { ...nextProps.options, type: nextProps.type } });
|
||||
}
|
||||
}
|
||||
|
||||
handleSave = data => {
|
||||
this.props.saveSettings(data.formData);
|
||||
this.setState({ changed: false });
|
||||
};
|
||||
|
||||
setFormData = (type, changed) => {
|
||||
let schema;
|
||||
if (type !== 'custom') {
|
||||
schema = {
|
||||
type: 'object',
|
||||
properties: { type: defaultSchema.properties.type }
|
||||
};
|
||||
} else {
|
||||
schema = defaultSchema;
|
||||
}
|
||||
return {
|
||||
formData: {
|
||||
type,
|
||||
...this.props.options
|
||||
},
|
||||
type,
|
||||
schema,
|
||||
changed
|
||||
};
|
||||
};
|
||||
|
||||
handleChange = data => {
|
||||
const formData = data.formData;
|
||||
const type = formData.type;
|
||||
if (type !== this.state.type) {
|
||||
this.setState(this.setFormData(type, true));
|
||||
} else if (!this.state.changed) {
|
||||
this.setState({ changed: true, formData });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const type = this.state.type || 'disabled';
|
||||
const changed = this.state.changed;
|
||||
const disabled = type === 'disabled';
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Form
|
||||
primaryButton={changed}
|
||||
noSubmit={disabled && !changed}
|
||||
submitText={disabled ? 'Disconnect' : 'Connect'}
|
||||
formData={this.state.formData}
|
||||
schema={this.state.schema}
|
||||
uiSchema={uiSchema}
|
||||
onChange={this.handleChange}
|
||||
onSubmit={this.handleSave}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return state.connection;
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
saveSettings: bindActionCreators(saveSocketSettings, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Connection);
|
|
@ -0,0 +1,64 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Container, Form, Button } from 'devui';
|
||||
import { listSchemes, listThemes } from 'devui/lib/utils/theme';
|
||||
import { changeTheme } from '../../actions';
|
||||
|
||||
class Themes extends Component {
|
||||
static propTypes = {
|
||||
changeTheme: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const theme = this.props.theme;
|
||||
const formData = {
|
||||
theme: theme.theme,
|
||||
scheme: theme.scheme,
|
||||
dark: !theme.light
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Form
|
||||
schema={{
|
||||
type: 'object',
|
||||
properties: {
|
||||
theme: {
|
||||
type: 'string',
|
||||
enum: listThemes(),
|
||||
},
|
||||
scheme: {
|
||||
title: 'color scheme',
|
||||
type: 'string',
|
||||
enum: listSchemes(),
|
||||
},
|
||||
dark: {
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
}}
|
||||
formData={formData}
|
||||
noSubmit
|
||||
onChange={this.props.changeTheme}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: state.theme
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
changeTheme: bindActionCreators(changeTheme, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Themes);
|
|
@ -0,0 +1,33 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Tabs } from 'devui';
|
||||
import Connection from './Connection';
|
||||
import Themes from './Themes';
|
||||
|
||||
class Settings extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.tabs = [
|
||||
{ name: 'Connection', component: Connection },
|
||||
{ name: 'Themes', component: Themes }
|
||||
];
|
||||
this.state = { selected: 'Connection' };
|
||||
}
|
||||
|
||||
handleSelect = selected => {
|
||||
this.setState({ selected });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Tabs
|
||||
toRight
|
||||
tabs={this.tabs}
|
||||
selected={this.state.selected}
|
||||
onClick={this.handleSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Settings;
|
102
packages/redux-devtools-core/src/app/components/TopButtons.js
Normal file
102
packages/redux-devtools-core/src/app/components/TopButtons.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ActionCreators } from 'redux-devtools-instrument';
|
||||
import { Button, Toolbar, Divider, Spacer } from 'devui';
|
||||
import RecordButton from './buttons/RecordButton';
|
||||
import PersistButton from './buttons/PersistButton';
|
||||
import LockButton from './buttons/LockButton';
|
||||
import InstanceSelector from './InstanceSelector';
|
||||
import SyncButton from './buttons/SyncButton';
|
||||
|
||||
const { reset, rollback, commit, sweep } = ActionCreators;
|
||||
|
||||
export default class TopButtons extends Component {
|
||||
static propTypes = {
|
||||
// shouldSync: PropTypes.bool,
|
||||
liftedState: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
options: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.options !== this.props.options
|
||||
|| nextProps.liftedState !== this.props.liftedState;
|
||||
}
|
||||
|
||||
handleRollback = () => {
|
||||
this.props.dispatch(rollback());
|
||||
};
|
||||
|
||||
handleSweep = () => {
|
||||
this.props.dispatch(sweep());
|
||||
};
|
||||
|
||||
handleCommit = () => {
|
||||
this.props.dispatch(commit());
|
||||
};
|
||||
|
||||
handleReset = () => {
|
||||
this.props.dispatch(reset());
|
||||
};
|
||||
|
||||
render() {
|
||||
const options = this.props.options;
|
||||
const features = options.features;
|
||||
const { computedStates, skippedActionIds, isPaused, isLocked } = this.props.liftedState;
|
||||
const noStates = computedStates.length < 2;
|
||||
|
||||
return (
|
||||
<Toolbar borderPosition="bottom">
|
||||
{features.pause &&
|
||||
<RecordButton paused={isPaused} />
|
||||
}
|
||||
{features.persist &&
|
||||
<PersistButton />
|
||||
}
|
||||
{features.lock &&
|
||||
<LockButton
|
||||
locked={isLocked}
|
||||
disabled={options.lib !== 'redux'}
|
||||
/>
|
||||
}
|
||||
<Divider />
|
||||
<Button
|
||||
title="Reset to the state you created the store with"
|
||||
tooltipPosition="bottom"
|
||||
onClick={this.handleReset}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
title="Roll back to the last committed state"
|
||||
tooltipPosition="bottom"
|
||||
onClick={this.handleRollback}
|
||||
disabled={noStates}
|
||||
>
|
||||
Revert
|
||||
</Button>
|
||||
<Button
|
||||
title="Remove all currently disabled actions from the log"
|
||||
tooltipPosition="bottom"
|
||||
onClick={this.handleSweep}
|
||||
disabled={skippedActionIds.length === 0}
|
||||
>
|
||||
Sweep
|
||||
</Button>
|
||||
<Button
|
||||
title="Remove all actions from the log,\a and make the current state your initial state"
|
||||
tooltipPosition="bottom"
|
||||
onClick={this.handleCommit}
|
||||
disabled={noStates}
|
||||
>
|
||||
Commit
|
||||
</Button>
|
||||
<Divider />
|
||||
<InstanceSelector />
|
||||
{features.sync &&
|
||||
<SyncButton />
|
||||
}
|
||||
</Toolbar>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button } from 'devui';
|
||||
import DispatchIcon from 'react-icons/lib/fa/terminal';
|
||||
import { toggleDispatcher } from '../../actions';
|
||||
|
||||
class DispatcherButton extends Component {
|
||||
static propTypes = {
|
||||
dispatcherIsOpen: PropTypes.bool,
|
||||
toggleDispatcher: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.dispatcherIsOpen !== this.props.dispatcherIsOpen;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button
|
||||
mark={this.props.dispatcherIsOpen && 'base0D'}
|
||||
title={this.props.dispatcherIsOpen ? 'Hide dispatcher' : 'Show dispatcher'}
|
||||
onClick={this.props.toggleDispatcher}
|
||||
tooltipPosition="top-left"
|
||||
>
|
||||
<DispatchIcon />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
toggleDispatcher: bindActionCreators(toggleDispatcher, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(DispatcherButton);
|
|
@ -0,0 +1,37 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button } from 'devui';
|
||||
import { stringify } from 'jsan';
|
||||
import DownloadIcon from 'react-icons/lib/ti/download';
|
||||
import { exportState } from '../../actions';
|
||||
|
||||
class ExportButton extends Component {
|
||||
static propTypes = {
|
||||
exportState: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button
|
||||
title="Export to a file"
|
||||
onClick={this.props.exportState}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
exportState: bindActionCreators(exportState, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(ExportButton);
|
|
@ -0,0 +1,65 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button } from 'devui';
|
||||
import UploadIcon from 'react-icons/lib/ti/upload';
|
||||
import { importState } from '../../actions';
|
||||
|
||||
class ImportButton extends Component {
|
||||
static propTypes = {
|
||||
importState: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.handleImport = this.handleImport.bind(this);
|
||||
this.handleImportFile = this.handleImportFile.bind(this);
|
||||
this.mapRef = this.mapRef.bind(this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
mapRef(node) {
|
||||
this.fileInput = node;
|
||||
}
|
||||
|
||||
handleImport() {
|
||||
this.fileInput.click();
|
||||
}
|
||||
|
||||
handleImportFile(e) {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
this.props.importState(reader.result);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
e.target.value = ''; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button
|
||||
title="Import from a file"
|
||||
onClick={this.handleImport}
|
||||
>
|
||||
<UploadIcon />
|
||||
<input
|
||||
type="file" ref={this.mapRef} style={{ display: 'none' }}
|
||||
onChange={this.handleImportFile}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
importState: bindActionCreators(importState, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(ImportButton);
|
|
@ -0,0 +1,40 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button } from 'devui';
|
||||
import LockIcon from 'react-icons/lib/io/ios-locked';
|
||||
import { lockChanges } from '../../actions';
|
||||
|
||||
class LockButton extends Component {
|
||||
static propTypes = {
|
||||
locked: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
lockChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.locked !== this.props.locked;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button
|
||||
tooltipPosition="bottom"
|
||||
disabled={this.props.disabled}
|
||||
mark={this.props.locked && 'base0D'}
|
||||
title={this.props.locked ? 'Unlock changes' : 'Lock changes'}
|
||||
onClick={this.props.lockChanges}
|
||||
>
|
||||
<LockIcon />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch, ownProps) {
|
||||
return {
|
||||
lockChanges: () => dispatch(lockChanges(!ownProps.locked))
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(LockButton);
|
|
@ -0,0 +1,48 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { Button } from 'devui';
|
||||
import PersistIcon from 'react-icons/lib/fa/thumb-tack';
|
||||
import { togglePersist } from '../../actions';
|
||||
|
||||
class LockButton extends Component {
|
||||
static propTypes = {
|
||||
persisted: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.persisted !== this.props.persisted;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button
|
||||
toolbar
|
||||
tooltipPosition="bottom"
|
||||
disabled={this.props.disabled}
|
||||
mark={this.props.persisted && 'base0D'}
|
||||
title={this.props.persisted ? 'Persist state history' : 'Disable state persisting'}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
<PersistIcon />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
persisted: state.instances.persisted
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onClick: bindActionCreators(togglePersist, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LockButton);
|
|
@ -0,0 +1,40 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from 'devui';
|
||||
import PrintIcon from 'react-icons/lib/md/print';
|
||||
|
||||
export default class PrintButton extends Component {
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
handlePrint() {
|
||||
const d3svg = document.getElementById('d3svg');
|
||||
if (!d3svg) {
|
||||
window.print();
|
||||
return;
|
||||
}
|
||||
|
||||
const initHeight = d3svg.style.height;
|
||||
const initWidth = d3svg.style.width;
|
||||
const box = d3svg.getBBox();
|
||||
d3svg.style.height = box.height;
|
||||
d3svg.style.width = box.width;
|
||||
|
||||
const g = d3svg.firstChild;
|
||||
const initTransform = g.getAttribute('transform');
|
||||
g.setAttribute('transform', initTransform.replace(/.+scale\(/, 'translate(57, 10) scale('));
|
||||
|
||||
window.print();
|
||||
|
||||
d3svg.style.height = initHeight;
|
||||
d3svg.style.width = initWidth;
|
||||
g.setAttribute('transform', initTransform);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button title="Print" onClick={this.handlePrint}><PrintIcon /></Button>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button } from 'devui';
|
||||
import RecordIcon from 'react-icons/lib/md/fiber-manual-record';
|
||||
import { pauseRecording } from '../../actions';
|
||||
|
||||
class RecordButton extends Component {
|
||||
static propTypes = {
|
||||
paused: PropTypes.bool,
|
||||
pauseRecording: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.paused !== this.props.paused;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button
|
||||
tooltipPosition="bottom-right"
|
||||
mark={!this.props.paused && 'base08'}
|
||||
title={this.props.paused ? 'Start recording' : 'Pause recording'}
|
||||
onClick={this.props.pauseRecording}
|
||||
>
|
||||
<RecordIcon />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch, ownProps) {
|
||||
return {
|
||||
pauseRecording: () => dispatch(pauseRecording(!ownProps.paused))
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(RecordButton);
|
|
@ -0,0 +1,39 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button } from 'devui';
|
||||
import HistoryIcon from 'react-icons/lib/md/av-timer';
|
||||
import { toggleSlider } from '../../actions';
|
||||
|
||||
class SliderButton extends Component {
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool,
|
||||
toggleSlider: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.isOpen !== this.props.isOpen;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button
|
||||
mark={this.props.isOpen && 'base0D'}
|
||||
title={this.props.isOpen ? 'Hide slider' : 'Show slider'}
|
||||
tooltipPosition="top-left"
|
||||
onClick={this.props.toggleSlider}
|
||||
>
|
||||
<HistoryIcon />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
toggleSlider: bindActionCreators(toggleSlider, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(SliderButton);
|
|
@ -0,0 +1,45 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button } from 'devui';
|
||||
import SyncIcon from 'react-icons/lib/ti/arrow-sync';
|
||||
import { toggleSync } from '../../actions';
|
||||
|
||||
class SyncButton extends Component {
|
||||
static propTypes = {
|
||||
sync: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.sync !== this.props.sync;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button
|
||||
title="Sync actions"
|
||||
tooltipPosition="bottom-left"
|
||||
onClick={this.props.onClick}
|
||||
mark={this.props.sync && 'base0B'}
|
||||
>
|
||||
<SyncIcon />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
sync: state.instances.sync
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onClick: bindActionCreators(toggleSync, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SyncButton);
|
|
@ -0,0 +1,24 @@
|
|||
export const CHANGE_SECTION = 'main/CHANGE_SECTION';
|
||||
export const CHANGE_THEME = 'main/CHANGE_THEME';
|
||||
|
||||
export const UPDATE_STATE = 'devTools/UPDATE_STATE';
|
||||
export const SET_STATE = 'devTools/SET_STATE';
|
||||
export const SELECT_INSTANCE = 'devTools/SELECT_INSTANCE';
|
||||
export const REMOVE_INSTANCE = 'devTools/REMOVE_INSTANCE';
|
||||
export const LIFTED_ACTION = 'devTools/LIFTED_ACTION';
|
||||
export const MONITOR_ACTION = 'devTools/MONITOR_ACTION';
|
||||
export const TOGGLE_SYNC = 'devTools/TOGGLE_SYNC';
|
||||
export const TOGGLE_PERSIST = 'devTools/TOGGLE_PERSIST';
|
||||
export const SELECT_MONITOR = 'devTools/SELECT_MONITOR';
|
||||
export const UPDATE_MONITOR_STATE = 'devTools/UPDATE_MONITOR_STATE';
|
||||
export const TOGGLE_SLIDER = 'devTools/TOGGLE_SLIDER';
|
||||
export const TOGGLE_DISPATCHER = 'devTools/TOGGLE_DISPATCHER';
|
||||
export const EXPORT = 'devTools/EXPORT';
|
||||
export const SHOW_NOTIFICATION = 'devTools/SHOW_NOTIFICATION';
|
||||
export const CLEAR_NOTIFICATION = 'devTools/CLEAR_NOTIFICATION';
|
||||
|
||||
export const UPDATE_REPORTS = 'reports/UPDATE';
|
||||
export const GET_REPORT_REQUEST = 'reports/GET_REPORT_REQUEST';
|
||||
export const GET_REPORT_ERROR = 'reports/GET_REPORT_ERROR';
|
||||
export const GET_REPORT_SUCCESS = 'reports/GET_REPORT_SUCCESS';
|
||||
export const ERROR = 'ERROR';
|
|
@ -0,0 +1,2 @@
|
|||
export const DATA_TYPE_KEY = Symbol.for('__serializedType__');
|
||||
export const DATA_REF_KEY = Symbol.for('__serializedRef__');
|
|
@ -0,0 +1,19 @@
|
|||
import socketCluster from 'socketcluster-client';
|
||||
|
||||
export const {
|
||||
CLOSED, CONNECTING, OPEN, AUTHENTICATED, PENDING, UNAUTHENTICATED
|
||||
} = socketCluster.SCSocket;
|
||||
export const CONNECT_REQUEST = 'socket/CONNECT_REQUEST';
|
||||
export const CONNECT_SUCCESS = 'socket/CONNECT_SUCCESS';
|
||||
export const CONNECT_ERROR = 'socket/CONNECT_ERROR';
|
||||
export const RECONNECT = 'socket/RECONNECT';
|
||||
export const AUTH_REQUEST = 'socket/AUTH_REQUEST';
|
||||
export const AUTH_SUCCESS = 'socket/AUTH_SUCCESS';
|
||||
export const AUTH_ERROR = 'socket/AUTH_ERROR';
|
||||
export const DISCONNECTED = 'socket/DISCONNECTED';
|
||||
export const DEAUTHENTICATE = 'socket/DEAUTHENTICATE';
|
||||
export const SUBSCRIBE_REQUEST = 'socket/SUBSCRIBE_REQUEST';
|
||||
export const SUBSCRIBE_SUCCESS = 'socket/SUBSCRIBE_SUCCESS';
|
||||
export const SUBSCRIBE_ERROR = 'socket/SUBSCRIBE_ERROR';
|
||||
export const UNSUBSCRIBE = 'socket/UNSUBSCRIBE';
|
||||
export const EMIT = 'socket/EMIT';
|
|
@ -0,0 +1,12 @@
|
|||
const socketOptions = {
|
||||
hostname: 'remotedev.io',
|
||||
port: 443,
|
||||
protocol: 'https',
|
||||
autoReconnect: true,
|
||||
secure: true,
|
||||
autoReconnectOptions: {
|
||||
randomness: 30000
|
||||
}
|
||||
};
|
||||
|
||||
export default socketOptions;
|
80
packages/redux-devtools-core/src/app/containers/Actions.js
Normal file
80
packages/redux-devtools-core/src/app/containers/Actions.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Container } from 'devui';
|
||||
import SliderMonitor from './monitors/Slider';
|
||||
import { liftedDispatch as liftedDispatchAction, getReport } from '../actions';
|
||||
import { getActiveInstance } from '../reducers/instances';
|
||||
import DevTools from '../containers/DevTools';
|
||||
import Dispatcher from './monitors/Dispatcher';
|
||||
import TopButtons from '../components/TopButtons';
|
||||
import BottomButtons from '../components/BottomButtons';
|
||||
|
||||
class Actions extends Component {
|
||||
render() {
|
||||
const {
|
||||
monitor, dispatcherIsOpen, sliderIsOpen, options, liftedState, liftedDispatch
|
||||
} = this.props;
|
||||
return (
|
||||
<Container>
|
||||
<TopButtons
|
||||
dispatch={liftedDispatch}
|
||||
liftedState={liftedState}
|
||||
options={options}
|
||||
/>
|
||||
<DevTools
|
||||
monitor={monitor}
|
||||
liftedState={liftedState}
|
||||
monitorState={this.props.monitorState}
|
||||
dispatch={liftedDispatch}
|
||||
features={options.features}
|
||||
/>
|
||||
{sliderIsOpen && options.connectionId && options.features.jump &&
|
||||
<SliderMonitor liftedState={liftedState} dispatch={liftedDispatch} />
|
||||
}
|
||||
{dispatcherIsOpen && options.connectionId && options.features.dispatch &&
|
||||
<Dispatcher options={options} />
|
||||
}
|
||||
<BottomButtons
|
||||
dispatcherIsOpen={dispatcherIsOpen}
|
||||
sliderIsOpen={sliderIsOpen}
|
||||
options={options}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Actions.propTypes = {
|
||||
liftedDispatch: PropTypes.func.isRequired,
|
||||
liftedState: PropTypes.object.isRequired,
|
||||
monitorState: PropTypes.object,
|
||||
options: PropTypes.object.isRequired,
|
||||
monitor: PropTypes.string,
|
||||
dispatcherIsOpen: PropTypes.bool,
|
||||
sliderIsOpen: PropTypes.bool
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const instances = state.instances;
|
||||
const id = getActiveInstance(instances);
|
||||
return {
|
||||
liftedState: instances.states[id],
|
||||
monitorState: state.monitor.monitorState,
|
||||
options: instances.options[id],
|
||||
monitor: state.monitor.selected,
|
||||
dispatcherIsOpen: state.monitor.dispatcherIsOpen,
|
||||
sliderIsOpen: state.monitor.sliderIsOpen,
|
||||
reports: state.reports.data
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
liftedDispatch: bindActionCreators(liftedDispatchAction, dispatch),
|
||||
getReport: bindActionCreators(getReport, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Actions);
|
58
packages/redux-devtools-core/src/app/containers/App.js
Normal file
58
packages/redux-devtools-core/src/app/containers/App.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { Container, Notification } from 'devui';
|
||||
import { clearNotification } from '../actions';
|
||||
import Header from '../components/Header';
|
||||
import Actions from '../containers/Actions';
|
||||
import Settings from '../components/Settings';
|
||||
|
||||
class App extends Component {
|
||||
render() {
|
||||
const { section, theme, notification } = this.props;
|
||||
let body;
|
||||
switch (section) {
|
||||
case 'Settings': body = <Settings />; break;
|
||||
default: body = <Actions />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container themeData={theme}>
|
||||
<Header section={section} />
|
||||
{body}
|
||||
{notification &&
|
||||
<Notification type={notification.type} onClose={this.props.clearNotification}>
|
||||
{notification.message}
|
||||
</Notification>
|
||||
}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
section: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
notification: PropTypes.shape({
|
||||
message: PropTypes.string,
|
||||
type: PropTypes.string
|
||||
}),
|
||||
clearNotification: PropTypes.func
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
section: state.section,
|
||||
theme: state.theme,
|
||||
notification: state.notification
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
clearNotification: bindActionCreators(clearNotification, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
89
packages/redux-devtools-core/src/app/containers/DevTools.js
Normal file
89
packages/redux-devtools-core/src/app/containers/DevTools.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
import React, { Component, createElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTheme } from 'styled-components';
|
||||
import getMonitor from '../utils/getMonitor';
|
||||
|
||||
class DevTools extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.getMonitor(props, props.monitorState);
|
||||
}
|
||||
|
||||
getMonitor(props, skipUpdate) {
|
||||
const monitorElement = getMonitor(props);
|
||||
this.monitorProps = monitorElement.props;
|
||||
this.Monitor = monitorElement.type;
|
||||
|
||||
const update = this.Monitor.update;
|
||||
if (update) {
|
||||
let newMonitorState;
|
||||
const monitorState = props.monitorState;
|
||||
if (skipUpdate || monitorState && monitorState.__overwritten__ === props.monitor) {
|
||||
newMonitorState = monitorState;
|
||||
} else {
|
||||
newMonitorState = update(this.monitorProps, undefined, {});
|
||||
if (newMonitorState !== monitorState) {
|
||||
this.preventRender = true;
|
||||
}
|
||||
}
|
||||
this.dispatch({
|
||||
type: '@@INIT_MONITOR',
|
||||
newMonitorState,
|
||||
update,
|
||||
monitorProps: this.monitorProps
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
if (nextProps.monitor !== this.props.monitor) this.getMonitor(nextProps);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return (
|
||||
nextProps.monitor !== this.props.monitor ||
|
||||
nextProps.liftedState !== this.props.liftedState ||
|
||||
nextProps.monitorState !== this.props.liftedState ||
|
||||
nextProps.features !== this.props.features ||
|
||||
nextProps.theme.scheme !== this.props.theme.scheme
|
||||
);
|
||||
}
|
||||
|
||||
dispatch = action => {
|
||||
this.props.dispatch(action);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.preventRender) {
|
||||
this.preventRender = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const liftedState = {
|
||||
...this.props.liftedState,
|
||||
monitorState: this.props.monitorState
|
||||
};
|
||||
return (
|
||||
<div className={`monitor monitor-${this.props.monitor}`}>
|
||||
<this.Monitor
|
||||
{...liftedState}
|
||||
{...this.monitorProps}
|
||||
features={this.props.features}
|
||||
dispatch={this.dispatch}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DevTools.propTypes = {
|
||||
liftedState: PropTypes.object,
|
||||
monitorState: PropTypes.object,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
monitor: PropTypes.string,
|
||||
features: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withTheme(DevTools);
|
|
@ -0,0 +1,55 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import ChartMonitor from 'redux-devtools-chart-monitor';
|
||||
import { selectMonitorWithState } from '../../actions';
|
||||
|
||||
export function getPath(obj, inspectedStatePath) {
|
||||
const parent = obj.parent;
|
||||
if (!parent) return;
|
||||
getPath(parent, inspectedStatePath);
|
||||
let name = obj.name;
|
||||
const item = name.match(/.+\[(\d+)]/);
|
||||
if (item) name = item[1];
|
||||
inspectedStatePath.push(name);
|
||||
}
|
||||
|
||||
class ChartMonitorWrapper extends Component {
|
||||
static update = ChartMonitor.update;
|
||||
|
||||
onClickText = (data) => {
|
||||
const inspectedStatePath = [];
|
||||
getPath(data, inspectedStatePath);
|
||||
this.props.selectMonitorWithState('InspectorMonitor', {
|
||||
inspectedStatePath,
|
||||
tabName: 'State',
|
||||
subTabName: data.children ? 'Chart' : 'Tree',
|
||||
selectedActionId: null,
|
||||
startActionId: null,
|
||||
inspectedActionPath: []
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ChartMonitor
|
||||
defaultIsVisible invertTheme
|
||||
onClickText={this.onClickText}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChartMonitorWrapper.propTypes = {
|
||||
selectMonitorWithState: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
selectMonitorWithState: bindActionCreators(selectMonitorWithState, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(ChartMonitorWrapper);
|
|
@ -0,0 +1,201 @@
|
|||
// Based on https://github.com/YoruNoHikage/redux-devtools-dispatch
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { Button, Select, Editor, Toolbar } from 'devui';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { dispatchRemotely } from '../../actions';
|
||||
|
||||
export const DispatcherContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
padding-top: 2px;
|
||||
background: ${props => props.theme.base01};
|
||||
`;
|
||||
|
||||
export const CodeContainer = styled.div`
|
||||
height: 75px;
|
||||
padding-right: 6px;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export const ActionContainer = styled.div`
|
||||
display: table;
|
||||
width: 100%;
|
||||
color: ${props => props.theme.base06};
|
||||
|
||||
> div {
|
||||
display: table-row;
|
||||
|
||||
> div:first-child {
|
||||
width: 1px;
|
||||
padding-left: 8px;
|
||||
display: table-cell;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> div:nth-child(2) {
|
||||
display: table-cell;
|
||||
width: 100%;
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
class Dispatcher extends Component {
|
||||
static propTypes = {
|
||||
options: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
state = {
|
||||
selected: 'default',
|
||||
customAction: this.props.options.lib === 'redux' ? '{\n type: \'\'\n}' : 'this.',
|
||||
args: [],
|
||||
rest: '[]',
|
||||
changed: false
|
||||
};
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.state.selected !== 'default' && !nextProps.options.actionCreators) {
|
||||
this.setState({
|
||||
selected: 'default',
|
||||
args: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextState !== this.state ||
|
||||
nextProps.options.actionCreators !== this.props.options.actionCreators;
|
||||
}
|
||||
|
||||
selectActionCreator = selected => {
|
||||
if (selected === 'actions-help') {
|
||||
window.open('https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/' +
|
||||
'basics/Dispatcher.md');
|
||||
return;
|
||||
}
|
||||
|
||||
const args = [];
|
||||
if (selected !== 'default') {
|
||||
args.length = this.props.options.actionCreators[selected].args.length;
|
||||
}
|
||||
this.setState({ selected, args, rest: '[]', changed: false });
|
||||
};
|
||||
|
||||
handleArg = argIndex => value => {
|
||||
const args = [
|
||||
...this.state.args.slice(0, argIndex),
|
||||
value || undefined,
|
||||
...this.state.args.slice(argIndex + 1),
|
||||
];
|
||||
this.setState({ args, changed: true });
|
||||
};
|
||||
|
||||
handleRest = rest => {
|
||||
this.setState({ rest, changed: true });
|
||||
};
|
||||
|
||||
handleCustomAction = customAction => {
|
||||
this.setState({ customAction, changed: true });
|
||||
};
|
||||
|
||||
dispatchAction = () => {
|
||||
const { selected, customAction, args, rest } = this.state;
|
||||
|
||||
if (this.state.selected !== 'default') {
|
||||
// remove trailing `undefined` arguments
|
||||
let i = args.length - 1;
|
||||
while (i >= 0 && typeof args[i] === 'undefined') {
|
||||
args.pop(i); i--;
|
||||
}
|
||||
this.props.dispatch({
|
||||
name: this.props.options.actionCreators[selected].name,
|
||||
selected,
|
||||
args,
|
||||
rest
|
||||
});
|
||||
} else {
|
||||
this.props.dispatch(customAction);
|
||||
}
|
||||
this.setState({ changed: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
const actionCreators = this.props.options.actionCreators;
|
||||
let actionElement;
|
||||
|
||||
if (this.state.selected === 'default' || !actionCreators) {
|
||||
actionElement = (
|
||||
<CodeContainer>
|
||||
<Editor
|
||||
value={this.state.customAction}
|
||||
onChange={this.handleCustomAction}
|
||||
/>
|
||||
</CodeContainer>
|
||||
);
|
||||
} else {
|
||||
actionElement = (
|
||||
<ActionContainer>
|
||||
{actionCreators[this.state.selected].args.map((param, i) => (
|
||||
<div key={`${param}${i}`}>
|
||||
<div>{param}</div>
|
||||
<Editor
|
||||
lineNumbers={false}
|
||||
value={this.state.args[i]}
|
||||
onChange={this.handleArg(i)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<div>...rest</div>
|
||||
<Editor
|
||||
lineNumbers={false}
|
||||
value={this.state.rest}
|
||||
onChange={this.handleRest}
|
||||
/>
|
||||
</div>
|
||||
</ActionContainer>
|
||||
);
|
||||
}
|
||||
|
||||
let options = [{ value: 'default', label: 'Custom action' }];
|
||||
if (actionCreators && actionCreators.length > 0) {
|
||||
options = options.concat(actionCreators.map(({ name, func, args }, i) => ({
|
||||
value: i,
|
||||
label: `${name}(${args.join(', ')})`
|
||||
})));
|
||||
} else {
|
||||
options.push({ value: 'actions-help', label: 'Add your app built-in actions…' });
|
||||
}
|
||||
|
||||
return (
|
||||
<DispatcherContainer>
|
||||
{actionElement}
|
||||
<Toolbar>
|
||||
<Select
|
||||
openOuterUp
|
||||
onChange={this.selectActionCreator}
|
||||
value={this.state.selected || 'default'}
|
||||
options={options}
|
||||
/>
|
||||
<Button onClick={this.dispatchAction} primary={this.state.changed}>Dispatch</Button>
|
||||
</Toolbar>
|
||||
</DispatcherContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
dispatch: bindActionCreators(dispatchRemotely, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(Dispatcher);
|
|
@ -0,0 +1,109 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { withTheme } from 'styled-components';
|
||||
import { tree } from 'd3-state-visualizer';
|
||||
import { getPath } from '../ChartMonitorWrapper';
|
||||
import { updateMonitorState } from '../../../actions';
|
||||
|
||||
const style = {
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
};
|
||||
|
||||
class ChartTab extends Component {
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.createChart(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
this.props.theme.scheme !== nextProps.theme.scheme ||
|
||||
nextProps.theme.light !== this.props.theme.light
|
||||
) {
|
||||
this.node.innerHTML = '';
|
||||
this.createChart(nextProps);
|
||||
} else if (nextProps.data !== this.props.data) {
|
||||
this.renderChart(nextProps.data);
|
||||
}
|
||||
}
|
||||
|
||||
getRef = node => {
|
||||
this.node = node;
|
||||
};
|
||||
|
||||
createChart(props) {
|
||||
this.renderChart = tree(this.node, this.getChartTheme(props.theme));
|
||||
this.renderChart(props.data);
|
||||
}
|
||||
|
||||
getChartTheme(theme) {
|
||||
return {
|
||||
heightBetweenNodesCoeff: 1,
|
||||
widthBetweenNodesCoeff: 1.3,
|
||||
tooltipOptions: {
|
||||
style: {
|
||||
color: theme.base06,
|
||||
'background-color': theme.base01,
|
||||
opacity: '0.9',
|
||||
'border-radius': '5px',
|
||||
padding: '5px'
|
||||
},
|
||||
offset: { left: 30, top: 10 },
|
||||
indentationSize: 2
|
||||
},
|
||||
style: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
node: {
|
||||
colors: {
|
||||
default: theme.base0B,
|
||||
collapsed: theme.base0B,
|
||||
parent: theme.base0E
|
||||
},
|
||||
radius: 7
|
||||
},
|
||||
text: {
|
||||
colors: {
|
||||
default: theme.base0D,
|
||||
hover: theme.base06
|
||||
}
|
||||
}
|
||||
},
|
||||
onClickText: this.onClickText
|
||||
};
|
||||
}
|
||||
|
||||
onClickText = (data) => {
|
||||
const inspectedStatePath = [];
|
||||
getPath(data, inspectedStatePath);
|
||||
this.props.updateMonitorState({
|
||||
inspectedStatePath,
|
||||
subTabName: data.children ? 'Chart' : 'Tree'
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return <div style={style} ref={this.getRef} />;
|
||||
}
|
||||
}
|
||||
|
||||
ChartTab.propTypes = {
|
||||
data: PropTypes.object,
|
||||
updateMonitorState: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
updateMonitorState: bindActionCreators(updateMonitorState, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
const ConnectedChartTab = connect(null, mapDispatchToProps)(ChartTab);
|
||||
export default withTheme(ConnectedChartTab);
|
|
@ -0,0 +1,29 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Editor } from 'devui';
|
||||
import stringify from 'javascript-stringify';
|
||||
|
||||
export default class RawTab extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.stringifyData(props);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.data !== this.value;
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
this.stringifyData(nextProps);
|
||||
}
|
||||
|
||||
stringifyData(props) {
|
||||
this.value = stringify(props.data, null, 2);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Editor value={this.value} />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Tabs } from 'devui';
|
||||
import StateTree from 'redux-devtools-inspector/lib/tabs/StateTab';
|
||||
import ActionTree from 'redux-devtools-inspector/lib/tabs/ActionTab';
|
||||
import DiffTree from 'redux-devtools-inspector/lib/tabs/DiffTab';
|
||||
import { selectMonitorTab } from '../../../actions';
|
||||
import RawTab from './RawTab';
|
||||
import ChartTab from './ChartTab';
|
||||
import VisualDiffTab from './VisualDiffTab';
|
||||
|
||||
class SubTabs extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.updateTabs(props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.parentTab !== this.props.parentTab) {
|
||||
this.updateTabs(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
selector = () => {
|
||||
switch (this.props.parentTab) {
|
||||
case 'Action':
|
||||
return { data: this.props.action };
|
||||
case 'Diff':
|
||||
return { data: this.props.delta };
|
||||
default:
|
||||
return { data: this.props.nextState };
|
||||
}
|
||||
};
|
||||
|
||||
updateTabs(props) {
|
||||
const parentTab = props.parentTab;
|
||||
|
||||
if (parentTab === 'Diff') {
|
||||
this.tabs = [
|
||||
{
|
||||
name: 'Tree',
|
||||
component: DiffTree,
|
||||
selector: () => this.props
|
||||
},
|
||||
{
|
||||
name: 'Raw',
|
||||
component: VisualDiffTab,
|
||||
selector: this.selector
|
||||
}
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
this.tabs = [
|
||||
{
|
||||
name: 'Tree',
|
||||
component: parentTab === 'Action' ? ActionTree : StateTree,
|
||||
selector: () => this.props
|
||||
},
|
||||
{
|
||||
name: 'Chart',
|
||||
component: ChartTab,
|
||||
selector: this.selector
|
||||
},
|
||||
{
|
||||
name: 'Raw',
|
||||
component: RawTab,
|
||||
selector: this.selector
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
let selected = this.props.selected;
|
||||
if (selected === 'Chart' && this.props.parentTab === 'Diff') selected = 'Tree';
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
tabs={this.tabs}
|
||||
selected={selected || 'Tree'}
|
||||
onClick={this.props.selectMonitorTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SubTabs.propTypes = {
|
||||
selected: PropTypes.string,
|
||||
parentTab: PropTypes.string,
|
||||
selectMonitorTab: PropTypes.func.isRequired,
|
||||
action: PropTypes.object,
|
||||
delta: PropTypes.object,
|
||||
nextState: PropTypes.object
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
parentTab: state.monitor.monitorState.tabName,
|
||||
selected: state.monitor.monitorState.subTabName
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
selectMonitorTab: bindActionCreators(selectMonitorTab, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SubTabs);
|
|
@ -0,0 +1,226 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { format } from 'jsondiffpatch/src/formatters/html';
|
||||
import styled from 'styled-components';
|
||||
import { effects } from 'devui';
|
||||
|
||||
export const StyledContainer = styled.div`
|
||||
.jsondiffpatch-delta {
|
||||
line-height: 14px;
|
||||
font-size: 12px;
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.jsondiffpatch-delta pre {
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
padding: 2px 3px;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
color: ${props => props.theme.base07};
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
ul.jsondiffpatch-delta {
|
||||
list-style-type: none;
|
||||
padding: 0 0 0 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.jsondiffpatch-delta ul {
|
||||
list-style-type: none;
|
||||
padding: 0 0 0 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.jsondiffpatch-left-value, .jsondiffpatch-right-value {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.jsondiffpatch-modified .jsondiffpatch-right-value:before {
|
||||
vertical-align: top;
|
||||
padding: 2px;
|
||||
color: ${props => props.theme.base0E};
|
||||
content: ' => ';
|
||||
}
|
||||
|
||||
.jsondiffpatch-added .jsondiffpatch-value pre,
|
||||
.jsondiffpatch-modified .jsondiffpatch-right-value pre,
|
||||
.jsondiffpatch-textdiff-added {
|
||||
background: ${props => effects.color(props.theme.base0B, 'alpha', 0.2)};
|
||||
}
|
||||
|
||||
.jsondiffpatch-deleted pre,
|
||||
.jsondiffpatch-modified .jsondiffpatch-left-value pre,
|
||||
.jsondiffpatch-textdiff-deleted {
|
||||
background: ${props => effects.color(props.theme.base08, 'alpha', 0.2)};
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.jsondiffpatch-unchanged,
|
||||
.jsondiffpatch-movedestination {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.jsondiffpatch-unchanged,
|
||||
.jsondiffpatch-movedestination > .jsondiffpatch-value {
|
||||
transition: all 0.5s;
|
||||
-webkit-transition: all 0.5s;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.jsondiffpatch-unchanged-showing .jsondiffpatch-unchanged,
|
||||
.jsondiffpatch-unchanged-showing .jsondiffpatch-movedestination > .jsondiffpatch-value {
|
||||
max-height: 100px;
|
||||
}
|
||||
|
||||
.jsondiffpatch-unchanged-hidden .jsondiffpatch-unchanged,
|
||||
.jsondiffpatch-unchanged-hidden .jsondiffpatch-movedestination > .jsondiffpatch-value {
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
.jsondiffpatch-unchanged-hiding .jsondiffpatch-movedestination > .jsondiffpatch-value,
|
||||
.jsondiffpatch-unchanged-hidden .jsondiffpatch-movedestination > .jsondiffpatch-value {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.jsondiffpatch-unchanged-visible .jsondiffpatch-unchanged,
|
||||
.jsondiffpatch-unchanged-visible .jsondiffpatch-movedestination > .jsondiffpatch-value {
|
||||
max-height: 100px;
|
||||
}
|
||||
|
||||
.jsondiffpatch-unchanged-hiding .jsondiffpatch-unchanged,
|
||||
.jsondiffpatch-unchanged-hiding .jsondiffpatch-movedestination > .jsondiffpatch-value {
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
.jsondiffpatch-unchanged-showing .jsondiffpatch-arrow,
|
||||
.jsondiffpatch-unchanged-hiding .jsondiffpatch-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.jsondiffpatch-value {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.jsondiffpatch-property-name {
|
||||
display: inline-block;
|
||||
padding: 2px 0;
|
||||
padding-right: 5px;
|
||||
vertical-align: top;
|
||||
color: ${props => props.theme.base0D};
|
||||
}
|
||||
|
||||
.jsondiffpatch-property-name:after {
|
||||
content: ': ';
|
||||
color: ${props => props.theme.base07};
|
||||
}
|
||||
|
||||
.jsondiffpatch-child-node-type-array > .jsondiffpatch-property-name:after {
|
||||
content: ': [';
|
||||
}
|
||||
|
||||
.jsondiffpatch-child-node-type-array:after {
|
||||
content: '],';
|
||||
}
|
||||
|
||||
div.jsondiffpatch-child-node-type-array:before {
|
||||
content: '[';
|
||||
}
|
||||
|
||||
div.jsondiffpatch-child-node-type-array:after {
|
||||
content: ']';
|
||||
}
|
||||
|
||||
.jsondiffpatch-child-node-type-object > .jsondiffpatch-property-name:after {
|
||||
content: ': {';
|
||||
}
|
||||
|
||||
.jsondiffpatch-child-node-type-object:after {
|
||||
content: '},';
|
||||
}
|
||||
|
||||
div.jsondiffpatch-child-node-type-object:before {
|
||||
content: '{';
|
||||
}
|
||||
|
||||
div.jsondiffpatch-child-node-type-object:after {
|
||||
content: '}';
|
||||
}
|
||||
|
||||
.jsondiffpatch-value pre:after {
|
||||
color: ${props => props.theme.base07};
|
||||
content: ',';
|
||||
}
|
||||
|
||||
li:last-child > .jsondiffpatch-value pre:after,
|
||||
.jsondiffpatch-modified > .jsondiffpatch-left-value pre:after {
|
||||
content: '';
|
||||
}
|
||||
|
||||
.jsondiffpatch-modified .jsondiffpatch-value {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.jsondiffpatch-modified .jsondiffpatch-right-value {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.jsondiffpatch-moved .jsondiffpatch-value {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.jsondiffpatch-moved .jsondiffpatch-moved-destination {
|
||||
display: inline-block;
|
||||
background: ${props => props.theme.base0A};
|
||||
}
|
||||
|
||||
.jsondiffpatch-moved .jsondiffpatch-moved-destination:before {
|
||||
content: ' => ';
|
||||
}
|
||||
|
||||
ul.jsondiffpatch-textdiff {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.jsondiffpatch-textdiff-location {
|
||||
display: inline-block;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.jsondiffpatch-textdiff-line {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.jsondiffpatch-textdiff-line-number:after {
|
||||
content: ',';
|
||||
}
|
||||
|
||||
.jsondiffpatch-error {
|
||||
background: red;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
|
||||
export default class VisualDiffTab extends Component {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return this.props.data !== nextProps.data;
|
||||
}
|
||||
|
||||
render() {
|
||||
let __html;
|
||||
const data = this.props.data;
|
||||
if (data) {
|
||||
__html = format(data);
|
||||
}
|
||||
|
||||
return <StyledContainer dangerouslySetInnerHTML={{ __html }} />;
|
||||
}
|
||||
}
|
||||
|
||||
VisualDiffTab.propTypes = {
|
||||
data: PropTypes.object
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import InspectorMonitor from 'redux-devtools-inspector';
|
||||
import StackTraceTab from 'redux-devtools-trace-monitor';
|
||||
import TestTab from 'redux-devtools-test-generator';
|
||||
import { DATA_TYPE_KEY } from '../../../constants/dataTypes';
|
||||
import SubTabs from './SubTabs';
|
||||
|
||||
const DEFAULT_TABS = [
|
||||
{
|
||||
name: 'Action',
|
||||
component: SubTabs
|
||||
}, {
|
||||
name: 'State',
|
||||
component: SubTabs
|
||||
}, {
|
||||
name: 'Diff',
|
||||
component: SubTabs
|
||||
},
|
||||
{
|
||||
name: 'Trace',
|
||||
component: StackTraceTab
|
||||
}
|
||||
];
|
||||
|
||||
class InspectorWrapper extends Component {
|
||||
static update = InspectorMonitor.update;
|
||||
|
||||
render() {
|
||||
const { features, ...rest } = this.props;
|
||||
let tabs;
|
||||
if (features && features.test) {
|
||||
tabs = () => [...DEFAULT_TABS, { name: 'Test', component: TestTab }];
|
||||
} else {
|
||||
tabs = () => DEFAULT_TABS;
|
||||
}
|
||||
|
||||
return (
|
||||
<InspectorMonitor
|
||||
dataTypeKey={DATA_TYPE_KEY}
|
||||
shouldPersistState={false}
|
||||
invertTheme={false}
|
||||
tabs={tabs}
|
||||
hideActionButtons={!features.skip}
|
||||
hideMainButtons
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InspectorWrapper.propTypes = {
|
||||
features: PropTypes.object
|
||||
};
|
||||
|
||||
export default InspectorWrapper;
|
|
@ -0,0 +1,39 @@
|
|||
import React, { Component, createElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import SliderMonitor from 'redux-slider-monitor';
|
||||
|
||||
const SliderWrapper = styled.div`
|
||||
border-color: ${props => props.theme.base02};
|
||||
border-style: solid;
|
||||
border-width: 1px 0;
|
||||
`;
|
||||
|
||||
class Slider extends Component {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return (
|
||||
nextProps.liftedState !== this.props.liftedState ||
|
||||
nextProps.theme.scheme !== this.props.theme.scheme
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<SliderWrapper>
|
||||
<SliderMonitor
|
||||
{...this.props.liftedState}
|
||||
dispatch={this.props.dispatch}
|
||||
theme={this.props.theme}
|
||||
hideResetButton
|
||||
/>
|
||||
</SliderWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Slider.propTypes = {
|
||||
liftedState: PropTypes.object,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withTheme(Slider);
|
40
packages/redux-devtools-core/src/app/index.js
Normal file
40
packages/redux-devtools-core/src/app/index.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import 'devui/lib/presets';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Provider } from 'react-redux';
|
||||
import configureStore from './store/configureStore';
|
||||
import { CONNECT_REQUEST } from './constants/socketActionTypes';
|
||||
import App from './containers/App';
|
||||
|
||||
class Root extends Component {
|
||||
componentWillMount() {
|
||||
configureStore((store, preloadedState) => {
|
||||
this.store = store;
|
||||
store.dispatch({
|
||||
type: CONNECT_REQUEST,
|
||||
options: preloadedState.connection || this.props.socketOptions
|
||||
});
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.store) return null;
|
||||
return (
|
||||
<Provider store={this.store}>
|
||||
<App {...this.props} />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Root.propTypes = {
|
||||
socketOptions: PropTypes.shape({
|
||||
hostname: PropTypes.string,
|
||||
port: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
autoReconnect: PropTypes.bool,
|
||||
secure: PropTypes.bool
|
||||
})
|
||||
};
|
||||
|
||||
export default Root;
|
201
packages/redux-devtools-core/src/app/middlewares/api.js
Normal file
201
packages/redux-devtools-core/src/app/middlewares/api.js
Normal file
|
@ -0,0 +1,201 @@
|
|||
import socketCluster from 'socketcluster-client';
|
||||
import { stringify } from 'jsan';
|
||||
import socketOptions from '../constants/socketOptions';
|
||||
import * as actions from '../constants/socketActionTypes';
|
||||
import { getActiveInstance } from '../reducers/instances';
|
||||
import {
|
||||
UPDATE_STATE, REMOVE_INSTANCE, LIFTED_ACTION,
|
||||
UPDATE_REPORTS, GET_REPORT_REQUEST, GET_REPORT_ERROR, GET_REPORT_SUCCESS
|
||||
} from '../constants/actionTypes';
|
||||
import { showNotification, importState } from '../actions';
|
||||
import { nonReduxDispatch } from '../utils/monitorActions';
|
||||
|
||||
let socket;
|
||||
let store;
|
||||
|
||||
function emit({ message: type, id, instanceId, action, state }) {
|
||||
socket.emit(
|
||||
id ? 'sc-' + id : 'respond',
|
||||
{ type, action, state, instanceId }
|
||||
);
|
||||
}
|
||||
|
||||
function startMonitoring(channel) {
|
||||
if (channel !== store.getState().socket.baseChannel) return;
|
||||
store.dispatch({ type: actions.EMIT, message: 'START' });
|
||||
}
|
||||
|
||||
function dispatchRemoteAction({ message, action, state, toAll }) {
|
||||
const instances = store.getState().instances;
|
||||
const instanceId = getActiveInstance(instances);
|
||||
const id = !toAll && instances.options[instanceId].connectionId;
|
||||
store.dispatch({
|
||||
type: actions.EMIT,
|
||||
message,
|
||||
action,
|
||||
state: nonReduxDispatch(store, message, instanceId, action, state, instances),
|
||||
instanceId,
|
||||
id
|
||||
});
|
||||
}
|
||||
|
||||
function monitoring(request) {
|
||||
if (request.type === 'DISCONNECTED') {
|
||||
store.dispatch({
|
||||
type: REMOVE_INSTANCE,
|
||||
id: request.id
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.type === 'START') {
|
||||
store.dispatch({ type: actions.EMIT, message: 'START', id: request.id });
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.type === 'ERROR') {
|
||||
store.dispatch(showNotification(request.payload));
|
||||
return;
|
||||
}
|
||||
|
||||
store.dispatch({
|
||||
type: UPDATE_STATE,
|
||||
request: request.data ? { ...request.data, id: request.id } : request
|
||||
});
|
||||
|
||||
const instances = store.getState().instances;
|
||||
const instanceId = request.instanceId || request.id;
|
||||
if (
|
||||
instances.sync && instanceId === instances.selected &&
|
||||
(request.type === 'ACTION' || request.type === 'STATE')
|
||||
) {
|
||||
socket.emit('respond', {
|
||||
type: 'SYNC',
|
||||
state: stringify(instances.states[instanceId]),
|
||||
id: request.id,
|
||||
instanceId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function subscribe(channelName, subscription) {
|
||||
const channel = socket.subscribe(channelName);
|
||||
if (subscription === UPDATE_STATE) channel.watch(monitoring);
|
||||
else {
|
||||
const watcher = request => {
|
||||
store.dispatch({ type: subscription, request });
|
||||
};
|
||||
channel.watch(watcher);
|
||||
socket.on(channelName, watcher);
|
||||
}
|
||||
}
|
||||
|
||||
function handleConnection() {
|
||||
socket.on('connect', status => {
|
||||
store.dispatch({
|
||||
type: actions.CONNECT_SUCCESS,
|
||||
payload: {
|
||||
id: status.id,
|
||||
authState: socket.authState,
|
||||
socketState: socket.state
|
||||
},
|
||||
error: status.authError
|
||||
});
|
||||
if (socket.authState !== actions.AUTHENTICATED) {
|
||||
store.dispatch({ type: actions.AUTH_REQUEST });
|
||||
}
|
||||
});
|
||||
socket.on('disconnect', code => {
|
||||
store.dispatch({ type: actions.DISCONNECTED, code });
|
||||
});
|
||||
|
||||
socket.on('subscribe', channel => {
|
||||
store.dispatch({ type: actions.SUBSCRIBE_SUCCESS, channel });
|
||||
});
|
||||
socket.on('unsubscribe', channel => {
|
||||
socket.unsubscribe(channel);
|
||||
socket.unwatch(channel);
|
||||
socket.off(channel);
|
||||
store.dispatch({ type: actions.UNSUBSCRIBE, channel });
|
||||
});
|
||||
socket.on('subscribeFail', error => {
|
||||
store.dispatch({ type: actions.SUBSCRIBE_ERROR, error, status: 'subscribeFail' });
|
||||
});
|
||||
socket.on('dropOut', error => {
|
||||
store.dispatch({ type: actions.SUBSCRIBE_ERROR, error, status: 'dropOut' });
|
||||
});
|
||||
|
||||
socket.on('error', error => {
|
||||
store.dispatch({ type: actions.CONNECT_ERROR, error });
|
||||
});
|
||||
}
|
||||
|
||||
function connect() {
|
||||
if (process.env.NODE_ENV === 'test') return;
|
||||
const connection = store.getState().connection;
|
||||
try {
|
||||
socket = socketCluster.connect(
|
||||
connection.type === 'remotedev' ? socketOptions : connection.options
|
||||
);
|
||||
handleConnection(store);
|
||||
} catch (error) {
|
||||
store.dispatch({ type: actions.CONNECT_ERROR, error });
|
||||
store.dispatch(showNotification(error.message || error));
|
||||
}
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
socket.disconnect();
|
||||
socket.off();
|
||||
}
|
||||
|
||||
function login() {
|
||||
socket.emit('login', {}, (error, baseChannel) => {
|
||||
if (error) {
|
||||
store.dispatch({ type: actions.AUTH_ERROR, error });
|
||||
return;
|
||||
}
|
||||
store.dispatch({ type: actions.AUTH_SUCCESS, baseChannel });
|
||||
store.dispatch({
|
||||
type: actions.SUBSCRIBE_REQUEST,
|
||||
channel: baseChannel,
|
||||
subscription: UPDATE_STATE
|
||||
});
|
||||
store.dispatch({
|
||||
type: actions.SUBSCRIBE_REQUEST,
|
||||
channel: 'report',
|
||||
subscription: UPDATE_REPORTS
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getReport(reportId) {
|
||||
socket.emit('getReport', reportId, (error, data) => {
|
||||
if (error) {
|
||||
store.dispatch({ type: GET_REPORT_ERROR, error });
|
||||
return;
|
||||
}
|
||||
store.dispatch({ type: GET_REPORT_SUCCESS, data });
|
||||
store.dispatch(importState(data.payload));
|
||||
});
|
||||
}
|
||||
|
||||
export default function api(inStore) {
|
||||
store = inStore;
|
||||
return next => action => {
|
||||
const result = next(action);
|
||||
switch (action.type) { // eslint-disable-line default-case
|
||||
case actions.CONNECT_REQUEST: connect(); break;
|
||||
case actions.RECONNECT:
|
||||
disconnect();
|
||||
if (action.options.type !== 'disabled') connect();
|
||||
break;
|
||||
case actions.AUTH_REQUEST: login(); break;
|
||||
case actions.SUBSCRIBE_REQUEST: subscribe(action.channel, action.subscription); break;
|
||||
case actions.SUBSCRIBE_SUCCESS: startMonitoring(action.channel); break;
|
||||
case actions.EMIT: if (socket) emit(action); break;
|
||||
case LIFTED_ACTION: dispatchRemoteAction(action); break;
|
||||
case GET_REPORT_REQUEST: getReport(action.report); break;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import stringifyJSON from '../utils/stringifyJSON';
|
||||
import { UPDATE_STATE, LIFTED_ACTION, EXPORT } from '../constants/actionTypes';
|
||||
import { getActiveInstance } from '../reducers/instances';
|
||||
|
||||
let toExport;
|
||||
|
||||
function download(state) {
|
||||
const blob = new Blob([state], { type: 'octet/stream' });
|
||||
const href = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.style = 'display: none';
|
||||
a.download = 'state.json';
|
||||
a.href = href;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(href);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
const exportState = store => next => action => {
|
||||
const result = next(action);
|
||||
|
||||
if (toExport && action.type === UPDATE_STATE && action.request.type === 'EXPORT') {
|
||||
const request = action.request;
|
||||
const id = request.instanceId || request.id;
|
||||
if (id === toExport) {
|
||||
toExport = undefined;
|
||||
download(JSON.stringify({
|
||||
payload: request.payload, preloadedState: request.committedState
|
||||
}, null, '\t'));
|
||||
}
|
||||
} else if (action.type === EXPORT) {
|
||||
const instances = store.getState().instances;
|
||||
const instanceId = getActiveInstance(instances);
|
||||
const options = instances.options[instanceId];
|
||||
if (options.features.export === true) {
|
||||
download(stringifyJSON(instances.states[instanceId], options.serialize));
|
||||
} else {
|
||||
toExport = instanceId;
|
||||
next({ type: LIFTED_ACTION, message: 'EXPORT', toExport: true });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export default exportState;
|
15
packages/redux-devtools-core/src/app/reducers/connection.js
Normal file
15
packages/redux-devtools-core/src/app/reducers/connection.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { RECONNECT } from '../constants/socketActionTypes';
|
||||
|
||||
export default function connection(
|
||||
state = {
|
||||
options: { hostname: 'localhost', port: 8000, secure: false },
|
||||
type: 'remotedev'
|
||||
},
|
||||
action
|
||||
) {
|
||||
if (action.type === RECONNECT) {
|
||||
const { type, ...options } = action.options;
|
||||
return { ...state, type, options };
|
||||
}
|
||||
return state;
|
||||
}
|
22
packages/redux-devtools-core/src/app/reducers/index.js
Normal file
22
packages/redux-devtools-core/src/app/reducers/index.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import section from './section';
|
||||
import connection from './connection';
|
||||
import socket from './socket';
|
||||
import monitor from './monitor';
|
||||
import notification from './notification';
|
||||
import instances from './instances';
|
||||
import reports from './reports';
|
||||
import theme from './theme';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
section,
|
||||
theme,
|
||||
connection,
|
||||
socket,
|
||||
monitor,
|
||||
instances,
|
||||
reports,
|
||||
notification
|
||||
});
|
||||
|
||||
export default rootReducer;
|
299
packages/redux-devtools-core/src/app/reducers/instances.js
Normal file
299
packages/redux-devtools-core/src/app/reducers/instances.js
Normal file
|
@ -0,0 +1,299 @@
|
|||
import {
|
||||
UPDATE_STATE, SET_STATE, LIFTED_ACTION,
|
||||
SELECT_INSTANCE, REMOVE_INSTANCE, TOGGLE_PERSIST, TOGGLE_SYNC
|
||||
} from '../constants/actionTypes';
|
||||
import { DISCONNECTED } from '../constants/socketActionTypes';
|
||||
import parseJSON from '../utils/parseJSON';
|
||||
import { recompute } from '../utils/updateState';
|
||||
|
||||
export const initialState = {
|
||||
selected: null,
|
||||
current: 'default',
|
||||
sync: false,
|
||||
connections: {},
|
||||
options: { default: { features: {} } },
|
||||
states: {
|
||||
default: {
|
||||
actionsById: {},
|
||||
computedStates: [],
|
||||
currentStateIndex: -1,
|
||||
nextActionId: 0,
|
||||
skippedActionIds: [],
|
||||
stagedActionIds: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function updateState(state, request, id, serialize) {
|
||||
let payload = request.payload;
|
||||
const actionsById = request.actionsById;
|
||||
if (actionsById) {
|
||||
payload = {
|
||||
...payload,
|
||||
actionsById: parseJSON(actionsById, serialize),
|
||||
computedStates: parseJSON(request.computedStates, serialize)
|
||||
};
|
||||
if (request.type === 'STATE' && request.committedState) {
|
||||
payload.committedState = payload.computedStates[0].state;
|
||||
}
|
||||
} else {
|
||||
payload = parseJSON(payload, serialize);
|
||||
}
|
||||
|
||||
let newState;
|
||||
const liftedState = state[id] || state.default;
|
||||
const action = request.action && parseJSON(request.action, serialize) || {};
|
||||
|
||||
switch (request.type) {
|
||||
case 'INIT':
|
||||
newState = recompute(
|
||||
state.default,
|
||||
payload,
|
||||
{ action: { type: '@@INIT' }, timestamp: action.timestamp || Date.now() }
|
||||
);
|
||||
break;
|
||||
case 'ACTION': {
|
||||
let isExcess = request.isExcess;
|
||||
const nextActionId = request.nextActionId || (liftedState.nextActionId + 1);
|
||||
const maxAge = request.maxAge;
|
||||
if (Array.isArray(action)) {
|
||||
// Batched actions
|
||||
newState = liftedState;
|
||||
for (let i = 0; i < action.length; i++) {
|
||||
newState = recompute(
|
||||
newState,
|
||||
request.batched ? payload : payload[i],
|
||||
action[i],
|
||||
newState.nextActionId + 1,
|
||||
maxAge,
|
||||
isExcess
|
||||
);
|
||||
}
|
||||
} else {
|
||||
newState = recompute(
|
||||
liftedState,
|
||||
payload,
|
||||
action,
|
||||
nextActionId,
|
||||
maxAge,
|
||||
isExcess
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'STATE':
|
||||
newState = payload;
|
||||
if (newState.computedStates.length <= newState.currentStateIndex) {
|
||||
newState.currentStateIndex = newState.computedStates.length - 1;
|
||||
}
|
||||
break;
|
||||
case 'PARTIAL_STATE': {
|
||||
const maxAge = request.maxAge;
|
||||
const nextActionId = payload.nextActionId;
|
||||
const stagedActionIds = payload.stagedActionIds;
|
||||
let computedStates = payload.computedStates;
|
||||
let oldActionsById;
|
||||
let oldComputedStates;
|
||||
let committedState;
|
||||
if (nextActionId > maxAge) {
|
||||
const oldStagedActionIds = liftedState.stagedActionIds;
|
||||
const excess = oldStagedActionIds.indexOf(stagedActionIds[1]);
|
||||
let key;
|
||||
if (excess > 0) {
|
||||
oldComputedStates = liftedState.computedStates.slice(excess - 1);
|
||||
oldActionsById = { ...liftedState.actionsById };
|
||||
for (let i = 1; i < excess; i++) {
|
||||
key = oldStagedActionIds[i];
|
||||
if (key) delete oldActionsById[key];
|
||||
}
|
||||
committedState = computedStates[0].state;
|
||||
} else {
|
||||
oldActionsById = liftedState.actionsById;
|
||||
oldComputedStates = liftedState.computedStates;
|
||||
committedState = liftedState.committedState;
|
||||
}
|
||||
} else {
|
||||
oldActionsById = liftedState.actionsById;
|
||||
oldComputedStates = liftedState.computedStates;
|
||||
committedState = liftedState.committedState;
|
||||
}
|
||||
computedStates = [...oldComputedStates, ...computedStates];
|
||||
const statesCount = computedStates.length;
|
||||
let currentStateIndex = payload.currentStateIndex;
|
||||
if (statesCount <= currentStateIndex) currentStateIndex = statesCount - 1;
|
||||
|
||||
newState = {
|
||||
...liftedState,
|
||||
actionsById: { ...oldActionsById, ...payload.actionsById },
|
||||
computedStates,
|
||||
currentStateIndex,
|
||||
nextActionId,
|
||||
stagedActionIds,
|
||||
committedState
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'LIFTED':
|
||||
newState = liftedState;
|
||||
break;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
if (request.liftedState) newState = { ...newState, ...request.liftedState };
|
||||
return { ...state, [id]: newState };
|
||||
}
|
||||
|
||||
export function dispatchAction(state, { action }) {
|
||||
if (action.type === 'JUMP_TO_STATE' || action.type === 'JUMP_TO_ACTION') {
|
||||
const id = state.selected || state.current;
|
||||
const liftedState = state.states[id];
|
||||
let currentStateIndex = action.index;
|
||||
if (typeof currentStateIndex === 'undefined' && action.actionId) {
|
||||
currentStateIndex = liftedState.stagedActionIds.indexOf(action.actionId);
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
states: {
|
||||
...state.states,
|
||||
[id]: { ...liftedState, currentStateIndex }
|
||||
}
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function removeState(state, connectionId) {
|
||||
const instanceIds = state.connections[connectionId];
|
||||
if (!instanceIds) return state;
|
||||
|
||||
const connections = { ...state.connections };
|
||||
const options = { ...state.options };
|
||||
const states = { ...state.states };
|
||||
let selected = state.selected;
|
||||
let current = state.current;
|
||||
let sync = state.sync;
|
||||
|
||||
delete connections[connectionId];
|
||||
instanceIds.forEach(id => {
|
||||
if (id === selected) {
|
||||
selected = null;
|
||||
sync = false;
|
||||
}
|
||||
if (id === current) {
|
||||
const inst = Object.keys(connections)[0];
|
||||
if (inst) current = connections[inst][0];
|
||||
else current = 'default';
|
||||
}
|
||||
delete options[id];
|
||||
delete states[id];
|
||||
});
|
||||
return {
|
||||
selected,
|
||||
current,
|
||||
sync,
|
||||
connections,
|
||||
options,
|
||||
states
|
||||
};
|
||||
}
|
||||
|
||||
function init({ type, action, name, libConfig = {} }, connectionId, current) {
|
||||
let lib;
|
||||
let actionCreators;
|
||||
let creators = libConfig.actionCreators || action;
|
||||
if (typeof creators === 'string') creators = JSON.parse(creators);
|
||||
if (Array.isArray(creators)) actionCreators = creators;
|
||||
if (type === 'STATE') lib = 'redux';
|
||||
return {
|
||||
name: libConfig.name || name || current,
|
||||
connectionId,
|
||||
explicitLib: libConfig.type,
|
||||
lib,
|
||||
actionCreators,
|
||||
features: libConfig.features ? libConfig.features :
|
||||
{
|
||||
lock: lib === 'redux',
|
||||
export: libConfig.type === 'redux' ? 'custom' : true,
|
||||
import: 'custom',
|
||||
persist: true,
|
||||
pause: true,
|
||||
reorder: true,
|
||||
jump: true,
|
||||
skip: true,
|
||||
dispatch: true,
|
||||
sync: true,
|
||||
test: true
|
||||
},
|
||||
serialize: libConfig.serialize
|
||||
};
|
||||
}
|
||||
|
||||
export default function instances(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case UPDATE_STATE: {
|
||||
const { request } = action;
|
||||
if (!request) return state;
|
||||
const connectionId = action.id || request.id;
|
||||
const current = request.instanceId || connectionId;
|
||||
let connections = state.connections;
|
||||
let options = state.options;
|
||||
|
||||
if (typeof state.options[current] === 'undefined') {
|
||||
connections = {
|
||||
...state.connections,
|
||||
[connectionId]: [...(connections[connectionId] || []), current]
|
||||
};
|
||||
options = { ...options, [current]: init(request, connectionId, current) };
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
current,
|
||||
connections,
|
||||
options,
|
||||
states: updateState(state.states, request, current, options[current].serialize)
|
||||
};
|
||||
}
|
||||
case SET_STATE:
|
||||
return {
|
||||
...state,
|
||||
states: {
|
||||
...state.states,
|
||||
[getActiveInstance(state)]: action.newState
|
||||
}
|
||||
};
|
||||
case TOGGLE_PERSIST:
|
||||
return { ...state, persisted: !state.persisted };
|
||||
case TOGGLE_SYNC:
|
||||
return { ...state, sync: !state.sync };
|
||||
case SELECT_INSTANCE:
|
||||
return { ...state, selected: action.selected, sync: false };
|
||||
case REMOVE_INSTANCE:
|
||||
return removeState(state, action.id);
|
||||
case LIFTED_ACTION: {
|
||||
if (action.message === 'DISPATCH') return dispatchAction(state, action);
|
||||
if (action.message === 'IMPORT') {
|
||||
const id = state.selected || state.current;
|
||||
if (state.options[id].features.import === true) {
|
||||
return {
|
||||
...state,
|
||||
states: {
|
||||
...state.states,
|
||||
[id]: parseJSON(action.state)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
case DISCONNECTED:
|
||||
return initialState;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable no-shadow */
|
||||
export const getActiveInstance = instances => instances.selected || instances.current;
|
||||
/* eslint-enable */
|
69
packages/redux-devtools-core/src/app/reducers/monitor.js
Normal file
69
packages/redux-devtools-core/src/app/reducers/monitor.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
import {
|
||||
MONITOR_ACTION, SELECT_MONITOR, SELECT_MONITOR_TAB, UPDATE_MONITOR_STATE,
|
||||
TOGGLE_SLIDER, TOGGLE_DISPATCHER
|
||||
} from '../constants/actionTypes';
|
||||
|
||||
const initialState = {
|
||||
selected: 'InspectorMonitor',
|
||||
monitorState: undefined,
|
||||
sliderIsOpen: true,
|
||||
dispatcherIsOpen: false
|
||||
};
|
||||
|
||||
export function dispatchMonitorAction(state, action) {
|
||||
return {
|
||||
...state,
|
||||
monitorState: action.action.newMonitorState ||
|
||||
action.monitorReducer(action.monitorProps, state.monitorState, action.action)
|
||||
};
|
||||
}
|
||||
|
||||
export default function monitor(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case MONITOR_ACTION:
|
||||
return dispatchMonitorAction(state, action);
|
||||
case SELECT_MONITOR: {
|
||||
let monitorState = state.monitorState;
|
||||
if (action.monitorState) {
|
||||
monitorState = {
|
||||
...action.monitorState,
|
||||
__overwritten__: action.monitor
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
monitorState,
|
||||
selected: action.monitor
|
||||
};
|
||||
}
|
||||
case UPDATE_MONITOR_STATE: {
|
||||
let inspectedStatePath = state.monitorState.inspectedStatePath;
|
||||
if (action.nextState.inspectedStatePath) {
|
||||
inspectedStatePath = [
|
||||
...inspectedStatePath.slice(0, -1),
|
||||
...action.nextState.inspectedStatePath
|
||||
];
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
monitorState: {
|
||||
...state.monitorState,
|
||||
...action.nextState,
|
||||
inspectedStatePath
|
||||
}
|
||||
};
|
||||
}
|
||||
case TOGGLE_SLIDER:
|
||||
return {
|
||||
...state,
|
||||
sliderIsOpen: !state.sliderIsOpen
|
||||
};
|
||||
case TOGGLE_DISPATCHER:
|
||||
return {
|
||||
...state,
|
||||
dispatcherIsOpen: !state.dispatcherIsOpen
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { SHOW_NOTIFICATION, CLEAR_NOTIFICATION, LIFTED_ACTION, ERROR } from '../constants/actionTypes';
|
||||
|
||||
export default function notification(state = null, action) {
|
||||
switch (action.type) {
|
||||
case SHOW_NOTIFICATION:
|
||||
return action.notification;
|
||||
case ERROR:
|
||||
return { type: 'error', message: action.payload };
|
||||
case LIFTED_ACTION:
|
||||
return null;
|
||||
case CLEAR_NOTIFICATION:
|
||||
return null;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
37
packages/redux-devtools-core/src/app/reducers/reports.js
Normal file
37
packages/redux-devtools-core/src/app/reducers/reports.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { UPDATE_REPORTS, GET_REPORT_SUCCESS } from '../constants/actionTypes';
|
||||
|
||||
const initialState = {
|
||||
data: []
|
||||
};
|
||||
|
||||
export default function reports(state = initialState, action) {
|
||||
/* if (action.type === GET_REPORT_SUCCESS) {
|
||||
const id = action.data.id;
|
||||
return {
|
||||
...state,
|
||||
data: state.data.map(d => (d.id === id ? action.data : d))
|
||||
};
|
||||
} else */ if (action.type !== UPDATE_REPORTS) return state;
|
||||
|
||||
const request = action.request;
|
||||
const data = request.data;
|
||||
switch (request.type) {
|
||||
case 'list':
|
||||
return {
|
||||
...state,
|
||||
data
|
||||
};
|
||||
case 'add':
|
||||
return {
|
||||
...state,
|
||||
data: [...state.data, data]
|
||||
};
|
||||
case 'remove':
|
||||
return {
|
||||
...state,
|
||||
data: state.data.filter(d => d.id !== request.id)
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
8
packages/redux-devtools-core/src/app/reducers/section.js
Normal file
8
packages/redux-devtools-core/src/app/reducers/section.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { CHANGE_SECTION } from '../constants/actionTypes';
|
||||
|
||||
export default function section(state = 'Actions', action) {
|
||||
if (action.type === CHANGE_SECTION) {
|
||||
return action.section;
|
||||
}
|
||||
return state;
|
||||
}
|
74
packages/redux-devtools-core/src/app/reducers/socket.js
Normal file
74
packages/redux-devtools-core/src/app/reducers/socket.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
import * as actions from '../constants/socketActionTypes';
|
||||
|
||||
const initialState = {
|
||||
id: null,
|
||||
channels: [],
|
||||
socketState: actions.CLOSED,
|
||||
authState: actions.PENDING,
|
||||
authToken: null,
|
||||
error: undefined
|
||||
};
|
||||
|
||||
export default function socket(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CONNECT_REQUEST: {
|
||||
return {
|
||||
...state,
|
||||
socketState: actions.CONNECTING
|
||||
};
|
||||
}
|
||||
case actions.CONNECT_ERROR:
|
||||
return {
|
||||
...state,
|
||||
error: action.error
|
||||
};
|
||||
case actions.CONNECT_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
id: action.payload.id,
|
||||
socketState: action.payload.socketState,
|
||||
authState: action.payload.authState,
|
||||
error: action.error
|
||||
};
|
||||
case actions.AUTH_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
authState: actions.PENDING
|
||||
};
|
||||
case actions.AUTH_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
authState: actions.AUTHENTICATED,
|
||||
authToken: action.authToken,
|
||||
baseChannel: action.baseChannel
|
||||
};
|
||||
case actions.AUTH_ERROR:
|
||||
return {
|
||||
...state,
|
||||
authState: actions.UNAUTHENTICATED,
|
||||
error: action.error
|
||||
};
|
||||
case actions.DEAUTHENTICATE:
|
||||
return {
|
||||
...state,
|
||||
authState: actions.UNAUTHENTICATED,
|
||||
authToken: null
|
||||
};
|
||||
case actions.SUBSCRIBE_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
channels: [...state.channels, action.channelName]
|
||||
};
|
||||
case actions.UNSUBSCRIBE:
|
||||
return {
|
||||
...state,
|
||||
channels: state.channels.filter(channel =>
|
||||
channel !== action.channelName
|
||||
)
|
||||
};
|
||||
case actions.DISCONNECTED:
|
||||
return initialState;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
15
packages/redux-devtools-core/src/app/reducers/theme.js
Normal file
15
packages/redux-devtools-core/src/app/reducers/theme.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { CHANGE_THEME } from '../constants/actionTypes';
|
||||
|
||||
export default function theme(
|
||||
state = { theme: 'default', scheme: 'default', light: true },
|
||||
action
|
||||
) {
|
||||
if (action.type === CHANGE_THEME) {
|
||||
return {
|
||||
theme: action.theme,
|
||||
scheme: action.scheme,
|
||||
light: !action.dark
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
39
packages/redux-devtools-core/src/app/store/configureStore.js
Normal file
39
packages/redux-devtools-core/src/app/store/configureStore.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { createStore, compose, applyMiddleware } from 'redux';
|
||||
import localForage from 'localforage';
|
||||
import { getStoredState, createPersistor } from 'redux-persist';
|
||||
import api from '../middlewares/api';
|
||||
import exportState from '../middlewares/exportState';
|
||||
import rootReducer from '../reducers';
|
||||
|
||||
export default function configureStore(callback, key) {
|
||||
const persistConfig = {
|
||||
keyPrefix: `redux-devtools${key || ''}:`,
|
||||
blacklist: ['instances', 'socket'],
|
||||
storage: localForage,
|
||||
serialize: data => data,
|
||||
deserialize: data => data
|
||||
};
|
||||
|
||||
getStoredState(persistConfig, (err, restoredState) => {
|
||||
let composeEnhancers = compose;
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) {
|
||||
composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
|
||||
}
|
||||
if (module.hot) {
|
||||
// Enable Webpack hot module replacement for reducers
|
||||
module.hot.accept('../reducers', () => {
|
||||
const nextReducer = require('../reducers'); // eslint-disable-line global-require
|
||||
store.replaceReducer(nextReducer);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const store = createStore(rootReducer, restoredState, composeEnhancers(
|
||||
applyMiddleware(exportState, api)
|
||||
));
|
||||
const persistor = createPersistor(store, persistConfig);
|
||||
callback(store, restoredState);
|
||||
if (err) persistor.purge();
|
||||
});
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// Based on https://github.com/gaearon/redux-devtools/pull/241
|
||||
/* eslint-disable no-param-reassign */
|
||||
|
||||
export default function commitExcessActions(liftedState, n = 1) {
|
||||
// Auto-commits n-number of excess actions.
|
||||
let excess = n;
|
||||
let idsToDelete = liftedState.stagedActionIds.slice(1, excess + 1);
|
||||
|
||||
for (let i = 0; i < idsToDelete.length; i++) {
|
||||
if (liftedState.computedStates[i + 1].error) {
|
||||
// Stop if error is found. Commit actions up to error.
|
||||
excess = i;
|
||||
idsToDelete = liftedState.stagedActionIds.slice(1, excess + 1);
|
||||
break;
|
||||
} else {
|
||||
delete liftedState.actionsById[idsToDelete[i]];
|
||||
}
|
||||
}
|
||||
|
||||
liftedState.skippedActionIds = liftedState.skippedActionIds.filter(
|
||||
id => idsToDelete.indexOf(id) === -1
|
||||
);
|
||||
liftedState.stagedActionIds = [0, ...liftedState.stagedActionIds.slice(excess + 1)];
|
||||
liftedState.committedState = liftedState.computedStates[excess].state;
|
||||
liftedState.computedStates = liftedState.computedStates.slice(excess);
|
||||
liftedState.currentStateIndex = liftedState.currentStateIndex > excess
|
||||
? liftedState.currentStateIndex - excess
|
||||
: 0;
|
||||
}
|
22
packages/redux-devtools-core/src/app/utils/getMonitor.js
Normal file
22
packages/redux-devtools-core/src/app/utils/getMonitor.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import LogMonitor from 'redux-devtools-log-monitor';
|
||||
import ChartMonitorWrapper from '../containers/monitors/ChartMonitorWrapper';
|
||||
import InspectorWrapper from '../containers/monitors/InspectorWrapper';
|
||||
|
||||
export const monitors = [
|
||||
{ value: 'InspectorMonitor', name: 'Inspector' },
|
||||
{ value: 'LogMonitor', name: 'Log monitor' },
|
||||
{ value: 'ChartMonitor', name: 'Chart' }
|
||||
];
|
||||
|
||||
export default function getMonitor({ monitor }) { // eslint-disable-line react/prop-types
|
||||
switch (monitor) {
|
||||
case 'LogMonitor':
|
||||
return <LogMonitor preserveScrollTop={false} hideMainButtons markStateDiff />;
|
||||
case 'ChartMonitor':
|
||||
return <ChartMonitorWrapper />;
|
||||
default:
|
||||
return <InspectorWrapper />;
|
||||
}
|
||||
}
|
50
packages/redux-devtools-core/src/app/utils/monitorActions.js
Normal file
50
packages/redux-devtools-core/src/app/utils/monitorActions.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
import difference from 'lodash/difference';
|
||||
import omit from 'lodash/omit';
|
||||
import stringifyJSON from './stringifyJSON';
|
||||
import { SET_STATE } from '../constants/actionTypes';
|
||||
|
||||
export function sweep(state) {
|
||||
return {
|
||||
...state,
|
||||
actionsById: omit(state.actionsById, state.skippedActionIds),
|
||||
stagedActionIds: difference(state.stagedActionIds, state.skippedActionIds),
|
||||
skippedActionIds: [],
|
||||
currentStateIndex: Math.min(state.currentStateIndex, state.stagedActionIds.length - 1)
|
||||
};
|
||||
}
|
||||
|
||||
export function nonReduxDispatch(store, message, instanceId, action, initialState, preInstances) {
|
||||
const instances = preInstances || store.getState().instances;
|
||||
const state = instances.states[instanceId];
|
||||
const options = instances.options[instanceId];
|
||||
|
||||
if (message !== 'DISPATCH') {
|
||||
if (message === 'IMPORT') {
|
||||
if (options.features.import === true) {
|
||||
return stringifyJSON(state.computedStates[state.currentStateIndex].state, true);
|
||||
}
|
||||
return initialState;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (options.lib === 'redux') return undefined;
|
||||
|
||||
switch (action.type) {
|
||||
case 'TOGGLE_ACTION':
|
||||
return stringifyJSON(state, true);
|
||||
case 'JUMP_TO_STATE':
|
||||
return stringifyJSON(state.computedStates[action.index].state, true);
|
||||
case 'JUMP_TO_ACTION':
|
||||
return stringifyJSON(
|
||||
state.computedStates[state.stagedActionIds.indexOf(action.actionId)].state, true
|
||||
);
|
||||
case 'ROLLBACK':
|
||||
return stringifyJSON(state.computedStates[0].state, true);
|
||||
case 'SWEEP':
|
||||
store.dispatch({ type: SET_STATE, newState: sweep(state) });
|
||||
return undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
34
packages/redux-devtools-core/src/app/utils/parseJSON.js
Normal file
34
packages/redux-devtools-core/src/app/utils/parseJSON.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import jsan from 'jsan';
|
||||
import { DATA_TYPE_KEY, DATA_REF_KEY } from '../constants/dataTypes';
|
||||
|
||||
export function reviver(key, value) {
|
||||
if (
|
||||
typeof value === 'object' && value !== null &&
|
||||
'__serializedType__' in value && typeof value.data === 'object'
|
||||
) {
|
||||
const data = value.data;
|
||||
data[DATA_TYPE_KEY] = value.__serializedType__;
|
||||
if ('__serializedRef__' in value) data[DATA_REF_KEY] = value.__serializedRef__;
|
||||
/*
|
||||
if (Array.isArray(data)) {
|
||||
data.__serializedType__ = value.__serializedType__;
|
||||
} else {
|
||||
Object.defineProperty(data, '__serializedType__', {
|
||||
value: value.__serializedType__
|
||||
});
|
||||
}
|
||||
*/
|
||||
return data;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export default function parseJSON(data, serialize) {
|
||||
if (typeof data !== 'string') return data;
|
||||
try {
|
||||
return serialize ? jsan.parse(data, reviver) : jsan.parse(data);
|
||||
} catch (e) {
|
||||
if (process.env.NODE_ENV !== 'production') console.error(data + 'is not a valid JSON', e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
17
packages/redux-devtools-core/src/app/utils/stringifyJSON.js
Normal file
17
packages/redux-devtools-core/src/app/utils/stringifyJSON.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import jsan from 'jsan';
|
||||
import { DATA_TYPE_KEY, DATA_REF_KEY } from '../constants/dataTypes';
|
||||
|
||||
function replacer(key, value) {
|
||||
if (typeof value === 'object' && value !== null && DATA_TYPE_KEY in value) {
|
||||
const __serializedType__ = value[DATA_TYPE_KEY];
|
||||
delete value[DATA_TYPE_KEY]; // eslint-disable-line no-param-reassign
|
||||
const r = { data: value, __serializedType__ };
|
||||
if (DATA_REF_KEY in value) r.__serializedRef__ = value[DATA_REF_KEY];
|
||||
return r;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export default function stringifyJSON(data, serialize) {
|
||||
return serialize ? jsan.stringify(data, replacer, null, true) : jsan.stringify(data);
|
||||
}
|
33
packages/redux-devtools-core/src/app/utils/updateState.js
Normal file
33
packages/redux-devtools-core/src/app/utils/updateState.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import commitExcessActions from './commitExcessActions';
|
||||
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export function recompute(previousLiftedState, storeState, action, nextActionId = 1, maxAge, isExcess) {
|
||||
const actionId = nextActionId - 1;
|
||||
const liftedState = { ...previousLiftedState };
|
||||
|
||||
if (liftedState.currentStateIndex === liftedState.stagedActionIds.length - 1) {
|
||||
liftedState.currentStateIndex++;
|
||||
}
|
||||
liftedState.stagedActionIds = [...liftedState.stagedActionIds, actionId];
|
||||
liftedState.actionsById = { ...liftedState.actionsById };
|
||||
if (action.type === 'PERFORM_ACTION') {
|
||||
liftedState.actionsById[actionId] = action;
|
||||
} else {
|
||||
liftedState.actionsById[actionId] = {
|
||||
action: action.action || action,
|
||||
timestamp: action.timestamp || Date.now(),
|
||||
stack: action.stack,
|
||||
type: 'PERFORM_ACTION'
|
||||
};
|
||||
}
|
||||
liftedState.nextActionId = nextActionId;
|
||||
liftedState.computedStates = [...liftedState.computedStates, { state: storeState }];
|
||||
|
||||
if (isExcess) commitExcessActions(liftedState);
|
||||
else if (maxAge) {
|
||||
const excess = liftedState.stagedActionIds.length - maxAge;
|
||||
if (excess > 0) commitExcessActions(liftedState, excess);
|
||||
}
|
||||
|
||||
return liftedState;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
const ERROR = '@@remotedev/ERROR';
|
||||
const ERROR = '@@redux-devtools/ERROR';
|
||||
|
||||
export default function catchErrors(sendError) {
|
||||
if (typeof window === 'object' && typeof window.onerror === 'object') {
|
||||
|
|
|
@ -35,21 +35,21 @@ export function getLocalFilter(config) {
|
|||
}
|
||||
|
||||
function getDevToolsOptions() {
|
||||
return typeof window !== 'undefined' && window.devToolsOptions || {};
|
||||
return (typeof window !== 'undefined' && window.devToolsOptions) || {};
|
||||
}
|
||||
|
||||
export function isFiltered(action, localFilter) {
|
||||
const { type } = action.action || action;
|
||||
const opts = getDevToolsOptions();
|
||||
if (
|
||||
!localFilter && (opts.filter && opts.filter === FilterState.DO_NOT_FILTER) ||
|
||||
type && typeof type.match !== 'function'
|
||||
(!localFilter && (opts.filter && opts.filter === FilterState.DO_NOT_FILTER)) ||
|
||||
(type && typeof type.match !== 'function')
|
||||
) return false;
|
||||
|
||||
const { whitelist, blacklist } = localFilter || opts;
|
||||
return (
|
||||
whitelist && !type.match(whitelist) ||
|
||||
blacklist && type.match(blacklist)
|
||||
(whitelist && !type.match(whitelist)) ||
|
||||
(blacklist && type.match(blacklist))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
1
packages/redux-devtools-core/test/__mocks__/styleMock.js
Normal file
1
packages/redux-devtools-core/test/__mocks__/styleMock.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = {};
|
42
packages/redux-devtools-core/test/app.spec.js
Normal file
42
packages/redux-devtools-core/test/app.spec.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
import { mount } from 'enzyme';
|
||||
// import { mountToJson } from 'enzyme-to-json';
|
||||
import App from '../src/app/containers/App';
|
||||
import api from '../src/app/middlewares/api';
|
||||
import exportState from '../src/app/middlewares/exportState';
|
||||
import rootReducer from '../src/app/reducers';
|
||||
|
||||
let wrapper;
|
||||
|
||||
const store = createStore(rootReducer, applyMiddleware(exportState, api));
|
||||
|
||||
describe('App container', () => {
|
||||
beforeAll(() => {
|
||||
wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
it('should render the App', () => {
|
||||
expect(mountToJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
*/
|
||||
|
||||
it('should render inspector monitor\'s wrapper', () => {
|
||||
expect(wrapper.find('DevtoolsInspector').html()).toBeDefined();
|
||||
});
|
||||
|
||||
it('should contain an empty action list', () => {
|
||||
expect(
|
||||
wrapper.find('ActionList').findWhere(n => {
|
||||
const { className } = n.props();
|
||||
return className && className.startsWith('actionListRows-');
|
||||
}).html()
|
||||
).toMatch(/<div class="actionListRows-\d-\d+"><\/div>/);
|
||||
});
|
||||
});
|
4
packages/redux-devtools-core/test/setup.js
Normal file
4
packages/redux-devtools-core/test/setup.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
84
packages/redux-devtools-core/webpack.config.js
Normal file
84
packages/redux-devtools-core/webpack.config.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
module.exports = (env = {}) => (
|
||||
{
|
||||
mode: 'development',
|
||||
entry: {
|
||||
app: './index.js'
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'build/' + env.platform),
|
||||
publicPath: '',
|
||||
filename: 'js/[name].js',
|
||||
sourceMapFilename: 'js/[name].map'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
loader: 'html-loader'
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{ loader: 'style-loader' },
|
||||
{ loader: 'css-loader' }
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|gif|jpg)$/,
|
||||
loader: 'url-loader',
|
||||
options: { limit: '25000', outputPath: 'images/', publicPath: 'images/' }
|
||||
},
|
||||
{
|
||||
test: /\.(ttf|eot|svg|woff|woff2)$/,
|
||||
loader: 'file-loader',
|
||||
options: { outputPath: 'fonts/', publicPath: 'fonts/' }
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: JSON.stringify(env.development ? 'development' : 'production'),
|
||||
PLATFORM: JSON.stringify(env.platform)
|
||||
}
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'assets/index.html'
|
||||
}),
|
||||
new CopyWebpackPlugin(
|
||||
env.platform === 'electron' ? [
|
||||
{ context: './src/electron', from: '*' }
|
||||
] : []
|
||||
)
|
||||
],
|
||||
optimization: {
|
||||
minimize: false,
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
vendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: 'common',
|
||||
chunks: 'all'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
devServer: {
|
||||
port: 3000
|
||||
},
|
||||
devtool: env.development ? 'eval' : 'source-map'
|
||||
}
|
||||
);
|
82
packages/redux-devtools-core/webpack.config.umd.js
Normal file
82
packages/redux-devtools-core/webpack.config.umd.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
|
||||
module.exports = (env = {}) => (
|
||||
{
|
||||
mode: 'production',
|
||||
entry: {
|
||||
app: ['./src/app/index.js']
|
||||
},
|
||||
output: {
|
||||
library: 'ReduxDevTools',
|
||||
libraryTarget: 'umd',
|
||||
path: path.resolve(__dirname, 'umd'),
|
||||
filename: env.minimize ? 'redux-devtools-core.min.js' : 'redux-devtools-core.js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
loader: 'html-loader'
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{ loader: 'style-loader' },
|
||||
{ loader: 'css-loader' }
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|gif|jpg)$/,
|
||||
loader: 'url-loader',
|
||||
options: { limit: '25000' }
|
||||
},
|
||||
{
|
||||
test: /\.(ttf|eot|svg|woff|woff2)$/,
|
||||
loader: 'url-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: JSON.stringify('production'),
|
||||
PLATFORM: JSON.stringify('web')
|
||||
}
|
||||
})
|
||||
],
|
||||
externals: {
|
||||
react: {
|
||||
root: 'React',
|
||||
commonjs2: 'react',
|
||||
commonjs: 'react',
|
||||
amd: 'react'
|
||||
},
|
||||
'react-dom': {
|
||||
root: 'ReactDOM',
|
||||
commonjs2: 'react-dom',
|
||||
commonjs: 'react-dom',
|
||||
amd: 'react-dom'
|
||||
}
|
||||
},
|
||||
optimization: {
|
||||
minimize: !!env.minimize,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
safari10: true
|
||||
}
|
||||
})
|
||||
]
|
||||
},
|
||||
performance: {
|
||||
hints: false
|
||||
}
|
||||
}
|
||||
);
|
|
@ -3,6 +3,7 @@
|
|||
"version": "0.11.0",
|
||||
"description": "Redux DevTools Diff Monitor",
|
||||
"scripts": {
|
||||
"build": "npm run build:lib",
|
||||
"build:lib": "NODE_ENV=production babel src --out-dir lib",
|
||||
"build:demo": "NODE_ENV=production webpack -p",
|
||||
"stats": "webpack --profile --json > stats.json",
|
||||
|
@ -44,7 +45,6 @@
|
|||
"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",
|
||||
|
@ -87,8 +87,5 @@
|
|||
"react-json-tree": "^0.11.1",
|
||||
"react-pure-render": "^1.0.2",
|
||||
"redux-devtools-themes": "^1.0.0"
|
||||
},
|
||||
"pre-commit": [
|
||||
"lint"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
"redux-devtools": "^3.3.2",
|
||||
"redux-devtools-dock-monitor": "^1.1.1",
|
||||
"redux-logger": "^2.8.1",
|
||||
"remotedev-inspector-monitor": "^0.11.0",
|
||||
"redux-devtools-inspector": "^0.11.0",
|
||||
"rimraf": "^2.5.2",
|
||||
"style-loader": "^0.13.1",
|
||||
"url-loader": "^0.5.7",
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
"clean": "rimraf lib",
|
||||
"build": "babel src --out-dir lib",
|
||||
"lint": "eslint src examples",
|
||||
"prepublish": "npm run lint && npm run clean && npm run build"
|
||||
"prepare": "npm run build",
|
||||
"prepublishOnly": "npm run lint && npm run clean && npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/reduxjs/redux-devtools"
|
||||
|
|
Loading…
Reference in New Issue
Block a user