Move zalmoxisus/remotedev-app

This commit is contained in:
Zalmoxisus 2019-01-03 16:14:25 +02:00
parent 8449c6a9fd
commit 9e9146690c
70 changed files with 4169 additions and 25 deletions

View File

@ -0,0 +1,4 @@
{
"presets": [ "es2015", "stage-0", "react" ],
"plugins": [ "add-module-exports", "transform-decorators-legacy" ]
}

View File

@ -0,0 +1,51 @@
{
"extends": "eslint-config-airbnb",
"globals": {
"chrome": true,
"electron": true
},
"env": {
"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" ],
"comma-style": [ 2, "last" ],
"eol-last": 0,
"no-unused-vars": 0,
"no-console": 0,
"func-names": 0,
"prefer-const": 0,
"comma-dangle": 0,
"id-length": 0,
"no-use-before-define": 0,
"indent": [2, 2, {"SwitchCase": 1}],
"new-cap": [2, { "capIsNewExceptions": ["Test"] }],
"no-underscore-dangle": 0,
"no-plusplus": 0,
"arrow-parens": 0,
"prefer-template": 0,
"class-methods-use-this": 0,
"max-len": ["error", { "code": 120 }],
"no-mixed-operators": 0,
"no-undef": 0
},
"plugins": [
"react"
]
}

View 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.

View File

@ -0,0 +1,44 @@
Remote Redux DevTools monitor app
==================================
![Demo](https://raw.githubusercontent.com/zalmoxisus/remote-redux-devtools/master/demo.gif)
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`, `remotedev-app` 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 'remotedev-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

View File

@ -0,0 +1,201 @@
const { app, BrowserWindow, Menu, shell } = require('electron');
let menu;
let template;
let win = null;
// require('electron-debug')();
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
app.on('ready', () => {
win = new BrowserWindow({ width: 1024, height: 728 });
win.loadURL(`file://${__dirname}/index.html`);
win.on('closed', () => {
win = null;
});
if (process.platform === 'darwin') {
template = [{
label: 'Electron',
submenu: [{
label: 'About',
selector: 'orderFrontStandardAboutPanel:'
}, {
type: 'separator'
}, {
label: 'Services',
submenu: []
}, {
type: 'separator'
}, {
label: 'Hide',
accelerator: 'Command+H',
selector: 'hide:'
}, {
label: 'Hide Others',
accelerator: 'Command+Shift+H',
selector: 'hideOtherApplications:'
}, {
label: 'Show All',
selector: 'unhideAllApplications:'
}, {
type: 'separator'
}, {
label: 'Quit',
accelerator: 'Command+Q',
click() {
app.quit();
}
}]
}, {
label: 'Edit',
submenu: [{
label: 'Undo',
accelerator: 'Command+Z',
selector: 'undo:'
}, {
label: 'Redo',
accelerator: 'Shift+Command+Z',
selector: 'redo:'
}, {
type: 'separator'
}, {
label: 'Cut',
accelerator: 'Command+X',
selector: 'cut:'
}, {
label: 'Copy',
accelerator: 'Command+C',
selector: 'copy:'
}, {
label: 'Paste',
accelerator: 'Command+V',
selector: 'paste:'
}, {
label: 'Select All',
accelerator: 'Command+A',
selector: 'selectAll:'
}]
}, {
label: 'View',
submenu: (process.env.NODE_ENV === 'development') ? [{
label: 'Reload',
accelerator: 'Command+R',
click() {
win.restart();
}
}, {
label: 'Toggle Full Screen',
accelerator: 'Ctrl+Command+F',
click() {
win.setFullScreen(!win.isFullScreen());
}
}, {
label: 'Toggle Developer Tools',
accelerator: 'Alt+Command+I',
click() {
win.toggleDevTools();
}
}] : [{
label: 'Toggle Full Screen',
accelerator: 'Ctrl+Command+F',
click() {
win.setFullScreen(!win.isFullScreen());
}
}]
}, {
label: 'Window',
submenu: [{
label: 'Minimize',
accelerator: 'Command+M',
selector: 'performMiniaturize:'
}, {
label: 'Close',
accelerator: 'Command+W',
selector: 'performClose:'
}, {
type: 'separator'
}, {
label: 'Bring All to Front',
selector: 'arrangeInFront:'
}]
}, {
label: 'Help',
submenu: [{
label: 'Learn More',
click() {
shell.openExternal('http://electron.atom.io');
}
}, {
label: 'Search Issues',
click() {
shell.openExternal('https://github.com/atom/electron/issues');
}
}]
}];
menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
} else {
template = [{
label: '&File',
submenu: [{
label: '&Open',
accelerator: 'Ctrl+O'
}, {
label: '&Close',
accelerator: 'Ctrl+W',
click() {
win.close();
}
}]
}, {
label: '&View',
submenu: (process.env.NODE_ENV === 'development') ? [{
label: '&Reload',
accelerator: 'Ctrl+R',
click() {
win.restart();
}
}, {
label: 'Toggle &Full Screen',
accelerator: 'F11',
click() {
win.setFullScreen(!win.isFullScreen());
}
}, {
label: 'Toggle &Developer Tools',
accelerator: 'Alt+Ctrl+I',
click() {
win.toggleDevTools();
}
}] : [{
label: 'Toggle &Full Screen',
accelerator: 'F11',
click() {
win.setFullScreen(!win.isFullScreen());
}
}]
}, {
label: 'Help',
submenu: [{
label: 'Learn More',
click() {
shell.openExternal('http://electron.atom.io');
}
}, {
label: 'Search Issues',
click() {
shell.openExternal('https://github.com/atom/electron/issues');
}
}]
}];
menu = Menu.buildFromTemplate(template);
win.setMenu(menu);
}
});

View File

@ -0,0 +1,9 @@
{
"name": "remotedev",
"productName": "RemoteDev",
"version": "0.0.1",
"electronVersion": "1.4.5",
"main": "index.js",
"description": "Remote Redux DevTools",
"authors": "Mihail Diordiev"
}

View File

@ -0,0 +1,17 @@
{
"osx" : {
"title": "RemoteDev",
"background": "osx/installer.png",
"icon": "osx/icon.icns",
"icon-size": 225,
"contents": [
{ "x": 290, "y": 1, "type": "link", "path": "/Applications" },
{ "x": 1, "y": 1, "type": "file" }
]
},
"win" : {
"title" : "RemoteDev",
"version" : "0.0.0.1",
"icon" : "windows/icon.ico"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title>RemoteDev</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>

View File

@ -0,0 +1,112 @@
{
"name": "remotedev-app",
"version": "0.11.0-3",
"description": "Remote Redux DevTools web, electron and chrome app.",
"scripts": {
"start": "webpack-dev-server --hot --inline --env.development --env.platform=web --progress",
"start:electron": "npm run build:electron && electron ./build/electron",
"build:electron": "rimraf ./build/electron && webpack -p --env.platform=electron --progress",
"build:web": "rimraf ./build/web && webpack -p --env.platform=web --progress",
"build:umd": "rimraf ./dist && webpack --env.development --progress --config webpack.config.umd.js",
"build:umd:min": "webpack -p --progress --config webpack.config.umd.js",
"clean": "rimraf ./build",
"build": "rimraf ./lib && babel ./src/app --out-dir lib",
"prepublish": "eslint ./src/app && npm run test && npm run build && npm run build:umd && npm run build:umd:min",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"test": "NODE_ENV=test jest --no-cache"
},
"main": "lib/index.js",
"files": [
"lib",
"dist"
],
"jest": {
"moduleNameMapper": {
"\\.(css|scss)$": "<rootDir>/test/__mocks__/styleMock.js"
}
},
"repository": {
"type": "git",
"url": "https://github.com/zalmoxisus/remotedev-app"
},
"homepage": "https://github.com/zalmoxisus/remotedev-app",
"keywords": [
"react",
"redux",
"devtools"
],
"author": "Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)",
"license": "MIT",
"devDependencies": {
"babel-cli": "^6.22.2",
"babel-core": "^6.22.1",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.10",
"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.22.0",
"babel-preset-react": "^6.22.0",
"babel-preset-stage-0": "^6.22.0",
"babel-register": "^6.22.0",
"chromedriver": "^2.20.0",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.26.1",
"electron": "^1.4.5",
"electron-builder": "^2.5.0",
"electron-debug": "^1.1.0",
"electron-packager": "^8.2.0",
"enzyme": "^2.8.2",
"enzyme-to-json": "^1.5.1",
"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": "^0.10.0",
"html-loader": "^0.4.4",
"html-webpack-plugin": "^2.28.0",
"jest": "^20.0.4",
"raw-loader": "^0.5.1",
"react": "^15.5.4",
"react-addons-test-utils": "15.4.0",
"react-dom": "^15.5.4",
"react-test-renderer": "^15.5.4",
"redux-immutable-state-invariant": "^1.2.0",
"redux-logger": "^2.2.1",
"rimraf": "^2.5.4",
"style-loader": "^0.13.0",
"url-loader": "^0.5.7",
"webpack": "^2.2.1",
"webpack-dev-server": "^2.3.0",
"webpack-hot-middleware": "^2.16.1"
},
"dependencies": {
"d3-state-visualizer": "^1.3.1",
"devui": "^1.0.0-2",
"javascript-stringify": "^1.5.0",
"jsan": "^3.1.9",
"jsondiffpatch": "^0.2.4",
"localforage": "^1.5.0",
"lodash": "^4.0.0",
"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-instrument": "^1.8.0",
"redux-devtools-log-monitor": "^1.3.0",
"redux-devtools-test-generator": "^0.5.1",
"redux-persist": "^4.8.0",
"redux-slider-monitor": "^2.0.0-0",
"remotedev-inspector-monitor": "^0.11.0",
"socketcluster-client": "^5.5.0",
"styled-components": "^2.0.0"
},
"peerDependencies": {
"react": "^15.4.0"
}
}

View 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 };
}

View File

@ -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>
);
}
}

View 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/RemoteDev')}
>
<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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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;

View 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>
);
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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>
);
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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';

View File

@ -0,0 +1,2 @@
export const DATA_TYPE_KEY = Symbol.for('__serializedType__');
export const DATA_REF_KEY = Symbol.for('__serializedRef__');

View File

@ -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';

View File

@ -0,0 +1,12 @@
const socketOptions = {
hostname: 'remotedev.io',
port: 443,
protocol: 'https',
autoReconnect: true,
secure: true,
autoReconnectOptions: {
randomness: 30000
}
};
export default socketOptions;

View 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);

View 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);

View File

@ -0,0 +1,88 @@
import React, { Component, PropTypes, createElement } from 'react';
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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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} />
);
}
}

View File

@ -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 'remotedev-inspector-monitor/lib/tabs/StateTab';
import ActionTree from 'remotedev-inspector-monitor/lib/tabs/ActionTab';
import DiffTree from 'remotedev-inspector-monitor/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);

View File

@ -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
};

View File

@ -0,0 +1,49 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import InspectorMonitor from 'remotedev-inspector-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
}];
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;

View File

@ -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);

View 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;

View 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;
};
}

View File

@ -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;

View 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;
}

View 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;

View 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 */

View 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;
}
}

View File

@ -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;
}
}

View 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;
}
}

View 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;
}

View 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;
}
}

View 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;
}

View 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: `remotedev${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();
});
}

View File

@ -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;
}

View 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 />;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}

View File

@ -0,0 +1,32 @@
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(),
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;
}

View File

@ -0,0 +1,25 @@
import React from 'react';
import { render } from 'react-dom';
import App from './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')
);
});
*/
}

View File

@ -0,0 +1 @@
module.exports = {};

View File

@ -0,0 +1,41 @@
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-[0-9]+"><\/div>/);
});
});

View File

@ -0,0 +1,82 @@
const path = require('path');
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = (env = {}) => (
{
entry: {
app: './src/index.js',
common: [
'react',
'react-dom',
'react-redux',
'redux',
'redux-persist',
'localforage',
'styled-components',
'jsan',
'socketcluster-client'
]
},
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 webpack.optimize.CommonsChunkPlugin({
names: ['common'],
}),
new HtmlWebpackPlugin({
template: 'assets/index.html'
}),
new CopyWebpackPlugin(
env.platform === 'electron' ? [
{ context: './src/electron', from: '*' }
] : []
)
],
devServer: {
port: 3000
},
devtool: env.development ? 'eval' : 'source-map'
}
);

View File

@ -0,0 +1,67 @@
const path = require('path');
const webpack = require('webpack');
module.exports = (env = {}) => (
{
entry: {
app: ['./src/app/index.js']
},
output: {
library: 'RemoteDevApp',
libraryTarget: 'umd',
path: path.resolve(__dirname, 'dist'),
filename: env.development ? 'remotedev-app.js' : 'remotedev-app.min.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'
}
}
}
);

253
yarn.lock
View File

@ -1726,7 +1726,7 @@ axobject-query@^2.0.1:
dependencies:
ast-types-flow "0.0.7"
babel-cli@^6.10.1, babel-cli@^6.24.1, babel-cli@^6.26.0, babel-cli@^6.3.15, babel-cli@^6.3.17, babel-cli@^6.4.5:
babel-cli@^6.10.1, babel-cli@^6.22.2, babel-cli@^6.24.1, babel-cli@^6.26.0, babel-cli@^6.3.15, babel-cli@^6.3.17, babel-cli@^6.4.5:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.26.0.tgz#502ab54874d7db88ad00b887a06383ce03d002f1"
integrity sha1-UCq1SHTX24itALiHoGODzgPQAvE=
@ -1809,7 +1809,7 @@ babel-core@^5.1.8, babel-core@^5.8.25, babel-core@^5.8.33:
trim-right "^1.0.0"
try-resolve "^1.0.0"
babel-core@^6.0.0, babel-core@^6.1.20, babel-core@^6.1.4, babel-core@^6.10.4, babel-core@^6.24.1, babel-core@^6.26.0, babel-core@^6.26.3, babel-core@^6.3.17, babel-core@^6.4.5:
babel-core@^6.0.0, babel-core@^6.1.20, babel-core@^6.1.4, babel-core@^6.10.4, babel-core@^6.22.1, babel-core@^6.24.1, babel-core@^6.26.0, babel-core@^6.26.3, babel-core@^6.3.17, babel-core@^6.4.5:
version "6.26.3"
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207"
integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==
@ -1887,7 +1887,7 @@ babel-eslint@^6.0.2, babel-eslint@^6.0.5, babel-eslint@^6.1.2:
lodash.assign "^4.0.0"
lodash.pickby "^4.0.0"
babel-eslint@^7.1.0, babel-eslint@^7.2.2:
babel-eslint@^7.1.0, babel-eslint@^7.1.1, babel-eslint@^7.2.2:
version "7.2.3"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.3.tgz#b2fe2d80126470f5c19442dc757253a897710827"
integrity sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=
@ -2115,7 +2115,7 @@ babel-jest@^23.6.0:
babel-plugin-istanbul "^4.1.6"
babel-preset-jest "^23.2.0"
babel-loader@^6.2.0, babel-loader@^6.2.2, babel-loader@^6.2.4, babel-loader@^6.4.1:
babel-loader@^6.2.0, babel-loader@^6.2.10, babel-loader@^6.2.2, babel-loader@^6.2.4, babel-loader@^6.4.1:
version "6.4.1"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-6.4.1.tgz#0b34112d5b0748a8dcdbf51acf6f9bd42d50b8ca"
integrity sha1-CzQRLVsHSKjc2/Uaz2+b1C1QuMo=
@ -2327,7 +2327,7 @@ babel-plugin-react-docgen@^1.9.0:
lodash "^4.17.0"
react-docgen "^3.0.0-beta11"
babel-plugin-react-transform@^2.0.0:
babel-plugin-react-transform@^2.0.0, babel-plugin-react-transform@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/babel-plugin-react-transform/-/babel-plugin-react-transform-2.0.2.tgz#515bbfa996893981142d90b1f9b1635de2995109"
integrity sha1-UVu/qZaJOYEULZCx+bFjXeKZUQk=
@ -2913,7 +2913,7 @@ babel-preset-env@1.6.1:
invariant "^2.2.2"
semver "^5.3.0"
babel-preset-env@^1.6.1, babel-preset-env@^1.7.0:
babel-preset-env@^1.1.8, babel-preset-env@^1.6.1, babel-preset-env@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a"
integrity sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==
@ -2982,7 +2982,7 @@ babel-preset-es2015-loose@^7.0.0:
dependencies:
modify-babel-preset "^1.0.0"
babel-preset-es2015@^6.24.1, babel-preset-es2015@^6.3.13, babel-preset-es2015@^6.9.0:
babel-preset-es2015@^6.22.0, babel-preset-es2015@^6.24.1, babel-preset-es2015@^6.3.13, babel-preset-es2015@^6.9.0:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939"
integrity sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=
@ -3083,7 +3083,7 @@ babel-preset-react-app@^3.1.2:
babel-preset-env "1.6.1"
babel-preset-react "6.24.1"
babel-preset-react@6.24.1, babel-preset-react@^6.24.1, babel-preset-react@^6.3.13, babel-preset-react@^6.5.0:
babel-preset-react@6.24.1, babel-preset-react@^6.22.0, babel-preset-react@^6.24.1, babel-preset-react@^6.3.13, babel-preset-react@^6.5.0:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-preset-react/-/babel-preset-react-6.24.1.tgz#ba69dfaea45fc3ec639b6a4ecea6e17702c91380"
integrity sha1-umnfrqRfw+xjm2pOzqbhdwLJE4A=
@ -3107,7 +3107,7 @@ babel-preset-react@6.3.13:
babel-plugin-transform-react-jsx "^6.3.13"
babel-plugin-transform-react-jsx-source "^6.3.13"
babel-preset-stage-0@^6.16.0, babel-preset-stage-0@^6.24.1, babel-preset-stage-0@^6.3.13, babel-preset-stage-0@^6.5.0:
babel-preset-stage-0@^6.16.0, babel-preset-stage-0@^6.22.0, babel-preset-stage-0@^6.24.1, babel-preset-stage-0@^6.3.13, babel-preset-stage-0@^6.5.0:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz#5642d15042f91384d7e5af8bc88b1db95b039e6a"
integrity sha1-VkLRUEL5E4TX5a+LyIsduVsDnmo=
@ -3146,7 +3146,7 @@ babel-preset-stage-3@^6.24.1:
babel-plugin-transform-exponentiation-operator "^6.24.1"
babel-plugin-transform-object-rest-spread "^6.22.0"
babel-register@^6.11.6, babel-register@^6.26.0:
babel-register@^6.11.6, babel-register@^6.22.0, babel-register@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
integrity sha1-btAhFz4vy0htestFxgCahW9kcHE=
@ -3245,6 +3245,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
base-64@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
integrity sha1-eAqZyE59YAJgNhURxId2E78k9rs=
base16@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70"
@ -4051,6 +4056,11 @@ clone-regexp@^1.0.0:
is-regexp "^1.0.0"
is-supported-regexp-flag "^1.0.0"
clone@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb"
integrity sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=
clone@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
@ -4269,6 +4279,11 @@ compare-func@^1.3.1:
array-ify "^1.0.0"
dot-prop "^3.0.0"
component-emitter@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.0.tgz#ccd113a86388d06482d03de3fc7df98526ba8efe"
integrity sha1-zNETqGOI0GSC0D3j/H35hSa6jv4=
component-emitter@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
@ -4489,6 +4504,20 @@ copy-descriptor@^0.1.0:
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
copy-webpack-plugin@^4.0.1:
version "4.6.0"
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.6.0.tgz#e7f40dd8a68477d405dd1b7a854aae324b158bae"
integrity sha512-Y+SQCF+0NoWQryez2zXn5J5knmr9z/9qSQt7fbL78u83rxmigOy8X5+BFn8CFSuX+nKT8gpYwJX68ekqtQt6ZA==
dependencies:
cacache "^10.0.4"
find-cache-dir "^1.0.0"
globby "^7.1.1"
is-glob "^4.0.0"
loader-utils "^1.1.0"
minimatch "^3.0.4"
p-limit "^1.0.0"
serialize-javascript "^1.4.0"
core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
@ -4885,6 +4914,23 @@ cyclist@~0.2.2:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
d3-state-visualizer@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/d3-state-visualizer/-/d3-state-visualizer-1.3.2.tgz#8e3ac418aa7ee7e3f46025309f9d1c215ee385eb"
integrity sha512-XgTRC6FXeoTt8l79cc2f3Zaah+K7DUQb3GL0zfbvoIi7zWWHV4l7OfuX9/JxxvwilKApMZwHMBJ7cJ2yWAc5IQ==
dependencies:
d3 "^3.5.6"
d3tooltip "^1.2.2"
deepmerge "^0.2.10"
is-plain-object "2.0.1"
map2tree "^1.4.0"
ramda "^0.17.1"
d3@^3.5.6:
version "3.5.17"
resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8"
integrity sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=
d@1:
version "1.0.0"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
@ -5026,6 +5072,11 @@ deep-is@~0.1.2, deep-is@~0.1.3:
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
deepmerge@^0.2.10:
version "0.2.10"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-0.2.10.tgz#8906bf9e525a4fbf1b203b2afcb4640249821219"
integrity sha1-iQa/nlJaT78bIDsq/LRkAkmCEhk=
default-require-extensions@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8"
@ -5762,7 +5813,7 @@ es6-template-regex@^0.1.1:
resolved "https://registry.yarnpkg.com/es6-template-regex/-/es6-template-regex-0.1.1.tgz#e517b9e0f742beeb8d3040834544fda0e4651467"
integrity sha1-5Re54PdCvuuNMECDRUT9oORlFGc=
es6-templates@^0.2.3:
es6-templates@^0.2.2, es6-templates@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/es6-templates/-/es6-templates-0.2.3.tgz#5cb9ac9fb1ded6eb1239342b81d792bbb4078ee4"
integrity sha1-XLmsn7He1usSOTQrgdeSu7QHjuQ=
@ -6095,7 +6146,7 @@ eslint-plugin-react@^5.2.2:
doctrine "^1.2.2"
jsx-ast-utils "^1.2.1"
eslint-plugin-react@^6.6.0:
eslint-plugin-react@^6.6.0, eslint-plugin-react@^6.9.0:
version "6.10.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-6.10.3.tgz#c5435beb06774e12c7db2f6abaddcbf900cd3f78"
integrity sha1-xUNb6wZ3ThLH2y9qut3L+QDNP3g=
@ -6322,7 +6373,7 @@ eslint@^2.13.1, eslint@^2.7.0:
text-table "~0.2.0"
user-home "^2.0.0"
eslint@^3.2.0:
eslint@^3.15.0, eslint@^3.2.0:
version "3.19.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc"
integrity sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=
@ -7579,6 +7630,18 @@ globby@^6.0.0, globby@^6.1.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
globby@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680"
integrity sha1-+yzP+UAfhgCUXfral0QMypcrhoA=
dependencies:
array-union "^1.0.1"
dir-glob "^2.0.0"
glob "^7.1.2"
ignore "^3.3.5"
pify "^3.0.0"
slash "^1.0.0"
globby@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50"
@ -7876,6 +7939,17 @@ html-entities@^1.2.0, html-entities@^1.2.1:
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=
html-loader@^0.4.4:
version "0.4.5"
resolved "https://registry.yarnpkg.com/html-loader/-/html-loader-0.4.5.tgz#5fbcd87cd63a5c49a7fce2fe56f425e05729c68c"
integrity sha1-X7zYfNY6XEmn/OL+VvQl4Fcpxow=
dependencies:
es6-templates "^0.2.2"
fastparse "^1.1.1"
html-minifier "^3.0.1"
loader-utils "^1.0.2"
object-assign "^4.1.0"
html-loader@^0.5.5:
version "0.5.5"
resolved "https://registry.yarnpkg.com/html-loader/-/html-loader-0.5.5.tgz#6356dbeb0c49756d8ebd5ca327f16ff06ab5faea"
@ -7887,7 +7961,7 @@ html-loader@^0.5.5:
loader-utils "^1.1.0"
object-assign "^4.1.1"
html-minifier@^3.2.3, html-minifier@^3.5.8:
html-minifier@^3.0.1, html-minifier@^3.2.3, html-minifier@^3.5.8:
version "3.5.21"
resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c"
integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==
@ -8110,6 +8184,11 @@ ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
immutable@3.7.6:
version "3.7.6"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b"
@ -8699,6 +8778,13 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
is-plain-object@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.1.tgz#4d7ca539bc9db9b737b8acb612f2318ef92f294f"
integrity sha1-TXylObydubc3uKy2EvIxjvkvKU8=
dependencies:
isobject "^1.0.0"
is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
@ -8831,6 +8917,11 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
isobject@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-1.0.2.tgz#f0f9b8ce92dd540fa0740882e3835a2e022ec78a"
integrity sha1-8Pm4zpLdVA+gdAiC44NaLgIux4o=
isobject@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
@ -8985,7 +9076,7 @@ jade@0.26.3:
commander "0.6.1"
mkdirp "0.3.0"
javascript-stringify@^1.1.0, javascript-stringify@^1.2.0:
javascript-stringify@^1.1.0, javascript-stringify@^1.2.0, javascript-stringify@^1.5.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-1.6.0.tgz#142d111f3a6e3dae8f4a9afd77d45855b5a9cce3"
integrity sha1-FC0RHzpuPa6PSpr9d9RYVbWpzOM=
@ -9598,7 +9689,7 @@ js-yaml@~3.7.0:
argparse "^1.0.7"
esprima "^2.6.0"
jsan@^3.1.13, jsan@^3.1.3:
jsan@^3.1.13, jsan@^3.1.3, jsan@^3.1.9:
version "3.1.13"
resolved "https://registry.yarnpkg.com/jsan/-/jsan-3.1.13.tgz#4de8c7bf8d1cfcd020c313d438f930cec4b91d86"
integrity sha512-9kGpCsGHifmw6oJet+y8HaCl14y7qgAsxVdV3pCHDySNR3BfDC30zgkssd7x5LRVAT22dnpbe9JdzzmXZnq9/g==
@ -9989,6 +10080,18 @@ libnpmaccess@^3.0.0:
npm-package-arg "^6.1.0"
npm-registry-fetch "^3.8.0"
lie@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=
dependencies:
immediate "~3.0.5"
linked-list@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/linked-list/-/linked-list-0.1.0.tgz#798b0ff97d1b92a4fd08480f55aea4e9d49d37bf"
integrity sha1-eYsP+X0bkqT9CEgPVa6k6dSdN78=
lint-staged@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-4.3.0.tgz#ed0779ad9a42c0dc62bb3244e522870b41125879"
@ -10133,6 +10236,13 @@ loader-utils@^1.1.0:
emojis-list "^2.0.0"
json5 "^1.0.1"
localforage@^1.5.0:
version "1.7.3"
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.7.3.tgz#0082b3ca9734679e1bd534995bdd3b24cf10f204"
integrity sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==
dependencies:
lie "3.1.1"
locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@ -10149,7 +10259,7 @@ locate-path@^3.0.0:
p-locate "^3.0.0"
path-exists "^3.0.0"
lodash-es@^4.2.1:
lodash-es@^4.17.4, lodash-es@^4.2.1:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.11.tgz#145ab4a7ac5c5e52a3531fb4f310255a152b4be0"
integrity sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q==
@ -12111,7 +12221,7 @@ p-is-promise@^1.1.0:
resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e"
integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=
p-limit@^1.1.0:
p-limit@^1.0.0, p-limit@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==
@ -13499,7 +13609,7 @@ react-icon-base@2.1.0:
resolved "https://registry.yarnpkg.com/react-icon-base/-/react-icon-base-2.1.0.tgz#a196e33fdf1e7aaa1fda3aefbb68bdad9e82a79d"
integrity sha1-oZbjP98eeqof2jrvu2i9rZ6Cp50=
react-icons@^2.2.3, react-icons@^2.2.7:
react-icons@^2.2.3, react-icons@^2.2.5, react-icons@^2.2.7:
version "2.2.7"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-2.2.7.tgz#d7860826b258557510dac10680abea5ca23cf650"
integrity sha512-0n4lcGqzJFcIQLoQytLdJCE0DKSA9dkwEZRYoGrIDJZFvIT6Hbajx5mv9geqhqFiNjUgtxg8kPyDfjlhymbGFg==
@ -13627,7 +13737,7 @@ react-pure-render@^1.0.2:
resolved "https://registry.yarnpkg.com/react-pure-render/-/react-pure-render-1.0.2.tgz#9d8a928c7f2c37513c2d064e57b3e3c356e9fabb"
integrity sha1-nYqSjH8sN1E8LQZOV7Pjw1bp+rs=
react-redux@^5.0.2:
react-redux@^5.0.2, react-redux@^5.0.5:
version "5.1.1"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.1.tgz#88e368682c7fa80e34e055cd7ac56f5936b0f52f"
integrity sha512-LE7Ned+cv5qe7tMV5BPYkGQ5Lpg8gzgItK07c67yHvJ8t0iaD9kPFPAli/mYkiyJYrs2pJgExR2ZgsGqlrOApg==
@ -13697,6 +13807,14 @@ react-style-proptype@^3.0.0:
dependencies:
prop-types "^15.5.4"
react-test-renderer@^15.5.4:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.6.2.tgz#d0333434fc2c438092696ca770da5ed48037efa8"
integrity sha1-0DM0NPwsQ4CSaWyncNpe1IA376g=
dependencies:
fbjs "^0.8.9"
object-assign "^4.1.0"
react-test-renderer@^16.0.0, react-test-renderer@^16.0.0-0, react-test-renderer@^16.4.0:
version "16.7.0"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.7.0.tgz#1ca96c2b450ab47c36ba92cd8c03fcefc52ea01c"
@ -14037,6 +14155,17 @@ reduce-function-call@^1.0.1:
dependencies:
balanced-match "^0.4.2"
redux-devtools-chart-monitor@^1.6.1:
version "1.7.0"
resolved "https://registry.yarnpkg.com/redux-devtools-chart-monitor/-/redux-devtools-chart-monitor-1.7.0.tgz#bf6356f480142e3576f5bbbeead433a1598c5e7f"
integrity sha512-1knxXASbo7ukukyf1rGNnME7gOgKY1XVZ4hoSzUjY6QFIC8iEneivXznCupxjfX5TDXLjZgQrFBrbGano1WK7g==
dependencies:
d3-state-visualizer "^1.3.1"
deepmerge "^0.2.10"
prop-types "^15.6.0"
react-pure-render "^1.0.2"
redux-devtools-themes "^1.0.0"
redux-devtools-dock-monitor@^1.0.1, redux-devtools-dock-monitor@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/redux-devtools-dock-monitor/-/redux-devtools-dock-monitor-1.1.3.tgz#1205e823c82536570aac8551a1c4b70972cba6aa"
@ -14055,14 +14184,31 @@ redux-devtools-themes@^1.0.0:
dependencies:
base16 "^1.0.0"
redux-logger@^2.5.2, redux-logger@^2.8.1:
redux-immutable-state-invariant@^1.2.0:
version "1.2.4"
resolved "https://registry.yarnpkg.com/redux-immutable-state-invariant/-/redux-immutable-state-invariant-1.2.4.tgz#e8bc4a37e22815375d5a04f8ecbb807054ea8bbb"
integrity sha1-6LxKN+IoFTddWgT47LuAcFTqi7s=
dependencies:
invariant "^2.1.0"
json-stringify-safe "^5.0.1"
redux-logger@^2.2.1, redux-logger@^2.5.2, redux-logger@^2.8.1:
version "2.10.2"
resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-2.10.2.tgz#3c5a5f0a6f32577c1deadf6655f257f82c6c3937"
integrity sha1-PFpfCm8yV3wd6t9mVfJX+CxsOTc=
dependencies:
deep-diff "0.3.4"
redux@^3.6.0, redux@^3.7.2:
redux-persist@^4.8.0:
version "4.10.2"
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-4.10.2.tgz#8efdb16cfe882c521a78a6d0bfdfef2437f49f96"
integrity sha512-U+e0ieMGC69Zr72929iJW40dEld7Mflh6mu0eJtVMLGfMq/aJqjxUM1hzyUWMR1VUyAEEdPHuQmeq5ti9krIgg==
dependencies:
json-stringify-safe "^5.0.1"
lodash "^4.17.4"
lodash-es "^4.17.4"
redux@^3.0.5, redux@^3.6.0, redux@^3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
integrity sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==
@ -14551,6 +14697,11 @@ safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, s
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-buffer@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
integrity sha1-0mPKVGls2KMGtcplUekt5XkY++c=
safe-regex@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
@ -14603,6 +14754,30 @@ sax@^1.2.1, sax@^1.2.4, sax@~1.2.1:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
sc-channel@~1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/sc-channel/-/sc-channel-1.0.6.tgz#b38bd47a993e78290fbc53467867f6b2a0a08639"
integrity sha1-s4vUepk+eCkPvFNGeGf2sqCghjk=
dependencies:
sc-emitter "1.x.x"
sc-emitter@1.x.x, sc-emitter@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/sc-emitter/-/sc-emitter-1.1.0.tgz#ef119d4222f4c64f887b486964ef11116cdd0e75"
integrity sha1-7xGdQiL0xk+Ie0hpZO8REWzdDnU=
dependencies:
component-emitter "1.2.0"
sc-errors@~1.3.0:
version "1.3.3"
resolved "https://registry.yarnpkg.com/sc-errors/-/sc-errors-1.3.3.tgz#c00bc4c766a970cc8d5937d08cd58e931d7dae05"
integrity sha1-wAvEx2apcMyNWTfQjNWOkx19rgU=
sc-formatter@~3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/sc-formatter/-/sc-formatter-3.0.2.tgz#9abdb14e71873ce7157714d3002477bbdb33c4e6"
integrity sha512-9PbqYBpCq+OoEeRQ3QfFIGE6qwjjBcd2j7UjgDlhnZbtSnuGgHdcRklPKYGuYFH82V/dwd+AIpu8XvA1zqTd+A==
scheduler@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.12.0.tgz#8ab17699939c0aedc5a196a657743c496538647b"
@ -14952,6 +15127,21 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
socketcluster-client@^5.5.0:
version "5.5.2"
resolved "https://registry.yarnpkg.com/socketcluster-client/-/socketcluster-client-5.5.2.tgz#9d4369e0e722ff7e55e5422c2d44f5afe1aff128"
integrity sha1-nUNp4Oci/35V5UIsLUT1r+Gv8Sg=
dependencies:
base-64 "0.1.0"
clone "2.1.1"
linked-list "0.1.0"
querystring "0.2.0"
sc-channel "~1.0.6"
sc-emitter "~1.1.0"
sc-errors "~1.3.0"
sc-formatter "~3.0.0"
ws "3.0.0"
sockjs-client@1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.5.tgz#1bb7c0f7222c40f42adf14f4442cbd1269771a83"
@ -15539,7 +15729,7 @@ strong-log-transformer@^2.0.0:
minimist "^1.2.0"
through "^2.3.4"
style-loader@^0.13.1:
style-loader@^0.13.0, style-loader@^0.13.1:
version "0.13.2"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.13.2.tgz#74533384cf698c7104c7951150b49717adc2f3bb"
integrity sha1-dFMzhM9pjHEEx5URULSXF63C87s=
@ -15574,7 +15764,7 @@ style-search@^0.1.0:
resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902"
integrity sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=
styled-components@^2.2.2:
styled-components@^2.0.0, styled-components@^2.2.2:
version "2.4.1"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-2.4.1.tgz#663bd0485d4b6ab46f946210dc03d2398d1ade74"
integrity sha1-ZjvQSF1LarRvlGIQ3APSOY0a3nQ=
@ -16286,6 +16476,11 @@ uid-number@0.0.6:
resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=
ultron@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==
umask@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d"
@ -16699,7 +16894,7 @@ webpack-dev-server@^2.3.0, webpack-dev-server@^2.4.1:
webpack-dev-middleware "1.12.2"
yargs "6.6.0"
webpack-hot-middleware@^2.22.1:
webpack-hot-middleware@^2.16.1, webpack-hot-middleware@^2.22.1:
version "2.24.3"
resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.24.3.tgz#5bb76259a8fc0d97463ab517640ba91d3382d4a6"
integrity sha512-pPlmcdoR2Fn6UhYjAhp1g/IJy1Yc9hD+T6O9mjRcWV2pFbBjIFoJXhP0CoD0xPOhWJuWXuZXGBga9ybbOdzXpg==
@ -16999,6 +17194,14 @@ write@^0.2.1:
dependencies:
mkdirp "^0.5.1"
ws@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-3.0.0.tgz#98ddb00056c8390cb751e7788788497f99103b6c"
integrity sha1-mN2wAFbIOQy3Ued4h4hJf5kQO2w=
dependencies:
safe-buffer "~5.0.1"
ultron "~1.1.0"
ws@^5.2.0:
version "5.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"