diff --git a/package.json b/package.json index 5d34cee..58fa266 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "react": "^15.2.1", "react-addons-test-utils": "^15.2.1", "react-dom": "^15.2.1", + "react-router": "^2.6.1", "react-transform-catch-errors": "^1.0.0", "react-transform-hmr": "^1.0.1", "redbox-react": "^1.2.0", @@ -92,10 +93,10 @@ "material-ui": "0.15.3", "react-icons": "^2.2.1", "react-redux": "^4.0.6", + "react-router-redux": "^4.0.5", "react-switcher": "^1.0.2", "react-tap-event-plugin": "^1.0.0", "redux": "^3.0.5", - "redux-devtools": "^3.2.0", "redux-devtools-chart-monitor": "^1.4.1", "redux-devtools-inspector": "^0.7.1", "redux-devtools-log-monitor": "^1.0.11", diff --git a/src/app/actions/index.js b/src/app/actions/index.js new file mode 100644 index 0000000..3880563 --- /dev/null +++ b/src/app/actions/index.js @@ -0,0 +1,55 @@ +import { + LIFTED_ACTION, MONITOR_ACTION, SELECT_INSTANCE, SELECT_MONITOR, + TOGGLE_SYNC, TOGGLE_SLIDER, TOGGLE_DISPATCHER, GET_REPORT_REQUEST, + SHOW_NOTIFICATION, CLEAR_NOTIFICATION +} from '../constants/actionTypes'; +import { RECONNECT } from '../constants/socketActionTypes'; + +export function liftedDispatch(action) { + if (action.type[0] === '@') return { type: MONITOR_ACTION, action }; + return { type: LIFTED_ACTION, message: 'DISPATCH', action }; +} + +export function selectInstance(event, index, selected) { + return { type: SELECT_INSTANCE, selected }; +} + +export function selectMonitor(event, index, value) { + return { type: SELECT_MONITOR, monitor: value }; +} + +export function importState(state) { + return { type: LIFTED_ACTION, message: 'IMPORT', state }; +} + +export function dispatchRemotely(action) { + return { type: LIFTED_ACTION, message: 'ACTION', action }; +} + +export function toggleSync() { + return { type: TOGGLE_SYNC }; +} + +export function toggleSlider() { + return { type: TOGGLE_SLIDER }; +} + +export function toggleDispatcher() { + return { type: TOGGLE_DISPATCHER }; +} + +export function saveSocketSettings(isCustom, options) { + return { type: RECONNECT, isCustom, 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 }; +} diff --git a/src/app/components/ButtonBar.js b/src/app/components/ButtonBar.js index 7b43e84..3537e82 100644 --- a/src/app/components/ButtonBar.js +++ b/src/app/components/ButtonBar.js @@ -11,23 +11,15 @@ import Settings from './Settings'; export default class ButtonBar extends Component { static propTypes = { - openModal: PropTypes.func.isRequired, - toggleDispatcher: PropTypes.func.isRequired, - toggleSlider: PropTypes.func.isRequired, + liftedState: PropTypes.object.isRequired, dispatcherIsOpen: PropTypes.bool, sliderIsOpen: PropTypes.bool, - closeModal: PropTypes.func.isRequired, - saveSettings: PropTypes.func.isRequired, - importState: PropTypes.func.isRequired, - exportState: PropTypes.func.isRequired, - socketOptions: PropTypes.object, noSettings: PropTypes.bool }; constructor() { super(); this.state = { settingsOpened: false }; - this.openSettings = this.openSettings.bind(this); this.closeSettings = this.closeSettings.bind(this); } @@ -53,23 +45,16 @@ export default class ButtonBar extends Component { render() { return (
- - - - + + + + {!this.props.noSettings && } {!this.props.noSettings && - + }
); diff --git a/src/app/components/Instances.js b/src/app/components/Instances.js index 47e5ad9..5566d5b 100644 --- a/src/app/components/Instances.js +++ b/src/app/components/Instances.js @@ -1,10 +1,13 @@ import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; import SelectField from 'material-ui/SelectField'; import MenuItem from 'material-ui/MenuItem'; import shallowCompare from 'react/lib/shallowCompare'; +import { selectInstance } from '../actions'; import styles from '../styles'; -export default class Instances extends Component { +class Instances extends Component { static propTypes = { selected: PropTypes.string, instances: PropTypes.object.isRequired, @@ -18,7 +21,7 @@ export default class Instances extends Component { render() { this.select = [['Autoselect instances', null]]; Object.keys(this.props.instances).forEach(key => { - this.select.push([this.props.instances[key], key]); + this.select.push([this.props.instances[key].name, key]); }); return ( @@ -38,3 +41,17 @@ export default class Instances extends Component { ); } } + +function mapStateToProps(state) { + return { + instances: state.instances.options + }; +} + +function mapDispatchToProps(dispatch) { + return { + onSelect: bindActionCreators(selectInstance, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(Instances); diff --git a/src/app/components/MonitorSelector.js b/src/app/components/MonitorSelector.js index 089f440..eb2887e 100644 --- a/src/app/components/MonitorSelector.js +++ b/src/app/components/MonitorSelector.js @@ -1,15 +1,22 @@ import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; import SelectField from 'material-ui/SelectField'; import MenuItem from 'material-ui/MenuItem'; -import { sideMonitors } from '../containers/DevTools'; +import { monitors } from '../containers/getMonitor'; +import { selectMonitor } from '../actions'; import styles from '../styles'; -export default class MonitorSelector extends Component { +class MonitorSelector extends Component { static propTypes = { selected: PropTypes.string, - onSelect: PropTypes.func.isRequired + selectMonitor: PropTypes.func.isRequired }; + items = monitors.map((item, i) => + + ); + shouldComponentUpdate(nextProps) { return nextProps.selected !== this.props.selected; } @@ -20,13 +27,19 @@ export default class MonitorSelector extends Component { style={styles.select} labelStyle={styles.selectLabel} iconStyle={styles.selectIcon} - onChange={this.props.onSelect} + onChange={this.props.selectMonitor} value={this.props.selected || 'InspectorMonitor'} > - {sideMonitors.map((item, i) => - - )} + {this.items} ); } } + +function mapDispatchToProps(dispatch) { + return { + selectMonitor: bindActionCreators(selectMonitor, dispatch) + }; +} + +export default connect(null, mapDispatchToProps)(MonitorSelector); diff --git a/src/app/components/Notification.js b/src/app/components/Notification.js new file mode 100644 index 0000000..214f289 --- /dev/null +++ b/src/app/components/Notification.js @@ -0,0 +1,59 @@ +import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import * as themes from 'redux-devtools-themes'; +import { clearNotification } from '../actions'; + +class Notification extends Component { + static propTypes = { + notification: PropTypes.shape({ + message: PropTypes.string, + type: PropTypes.string + }), + clearNotification: PropTypes.func.isRequired + }; + + shouldComponentUpdate(nextProps) { + return nextProps.notification !== this.props.notification; + } + + render() { + if (!this.props.notification) return null; + const theme = themes.nicinabox; + const buttonStyle = { + color: theme.base06, backgroundColor: theme.base00, + margin: '0', background: '#DC2424' + }; + const containerStyle = { + color: theme.base06, background: '#FC2424', + padding: '5px 10px', minHeight: '20px', display: 'flex' + }; + return ( +
+
+

{this.props.notification.message}

+
+
+ +
+
+ ); + } +} + +function mapStateToProps(state) { + return { + notification: state.notification + }; +} + +function mapDispatchToProps(dispatch) { + return { + clearNotification: bindActionCreators(clearNotification, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(Notification); diff --git a/src/app/components/Settings.js b/src/app/components/Settings.js index 2609766..408340b 100644 --- a/src/app/components/Settings.js +++ b/src/app/components/Settings.js @@ -1,39 +1,41 @@ import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; import Checkbox from 'material-ui/Checkbox'; import TextField from 'material-ui/TextField'; +import { saveSocketSettings } from '../actions'; import styles from '../styles'; -export default class Settings extends Component { +class Settings extends Component { static propTypes = { isOpen: PropTypes.bool, close: PropTypes.func.isRequired, saveSettings: PropTypes.func.isRequired, - socketOptions: PropTypes.object + socketOptions: PropTypes.object.isRequired, + isCustom: PropTypes.bool }; constructor(props) { super(props); - let isLocal; + const isCustom = props.isCustom; this.options = {}; - if (props.socketOptions) { - isLocal = true; + if (isCustom) { this.options.hostname = props.socketOptions.hostname; this.options.port = props.socketOptions.port; this.options.secure = props.socketOptions.secure; } else { - isLocal = false; this.options.hostname = 'localhost'; this.options.port = '8000'; this.options.secure = false; } - this.state = { isLocal }; + this.state = { isCustom }; } handleLocalChecked = (e, checked) => { - this.setState({ isLocal: checked }); + this.setState({ isCustom: checked }); }; handleInputChange = (e, value) => { @@ -45,7 +47,7 @@ export default class Settings extends Component { }; save = () => { - this.props.saveSettings(this.state.isLocal, this.options); + this.props.saveSettings(this.state.isCustom, this.options); this.props.close(); }; @@ -77,10 +79,10 @@ export default class Settings extends Component { > - {this.state.isLocal &&
+ {this.state.isCustom &&
{ - selected = saveToStorage('test-templates-sel', value); - this.setState({ selected }); + this.props.dispatch({ type: TEST_SELECT, selected: value }); }; - getDefaultTemplates(isRedux = this.props.isRedux) { - if (isRedux) return [mochaTemplate, tapeTemplate, avaTemplate]; + getDefaultTemplates() { + if (this.props.options.lib === 'redux') { + return [mochaTemplate, tapeTemplate, avaTemplate]; + } return [mochaVTemplate, tapeVTemplate, avaVTemplate]; } @@ -68,33 +43,25 @@ export default class TestGen extends Component { this.setState({ dialogStatus: 2 }); }; - handleSave = (template) => { - testTemplates = [...this.state.testTemplates]; - selected = this.state.selected; + dispatch = (action) => { + let templates; + if (!this.props.templates) templates = this.getDefaultTemplates(); + this.props.dispatch({ ...action, templates }); + }; + handleSave = (template) => { if (this.state.dialogStatus === 1) { - testTemplates[this.state.selected] = template; + this.dispatch({ type: TEST_EDIT, template }); } else { - testTemplates.push(template); - selected = testTemplates.length - 1; - saveToStorage('test-templates-sel', selected); + this.dispatch({ type: TEST_ADD, template }); } - - saveToStorage('test-templates', testTemplates); - this.setState({ testTemplates, selected, dialogStatus: 0 }); - isDefaultTemplate = false; + this.handleCloseDialog(); }; handleRemove = () => { // Todo: add snackbar with undo - selected = 0; - testTemplates = [...this.state.testTemplates]; - testTemplates.splice(this.state.selected, 1); - if (testTemplates.length === 0) testTemplates = this.getDefaultTemplates(); - saveToStorage('test-templates-sel', selected); - saveToStorage('test-templates', testTemplates); - this.setState({ testTemplates, selected, dialogStatus: 0 }); - isDefaultTemplate = false; + this.dispatch({ type: TEST_REMOVE }); + this.handleCloseDialog(); }; handleCloseDialog = () => { @@ -102,15 +69,18 @@ export default class TestGen extends Component { }; render() { - const { dialogStatus, selected, testTemplates } = this.state; // eslint-disable-line - const template = testTemplates[selected]; + const { dialogStatus } = this.state; + const { selected } = this.props; + const templates = this.props.templates || this.getDefaultTemplates(); + const template = templates[selected]; const { assertion, dispatcher, wrap } = template; return ( - {testTemplates.map((item, i) => + {templates.map((item, i) => )} @@ -142,14 +112,19 @@ export default class TestGen extends Component { } TestGen.propTypes = { - isRedux: PropTypes.bool, - testTemplates: PropTypes.oneOfType([ - PropTypes.array, - PropTypes.string - ]), - selectedTemplate: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.string - ]), - useCodemirror: PropTypes.bool + templates: PropTypes.array, + selected: PropTypes.number, + options: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired }; + +function mapStateToProps(state) { + const instances = state.instances; + return { + templates: state.test.templates, + selected: state.test.selected, + options: instances.options[getActiveInstance(instances)] + }; +} + +export default connect(mapStateToProps)(TestGen); diff --git a/src/app/components/buttons/DispatcherButton.js b/src/app/components/buttons/DispatcherButton.js index 08ad928..252c9e9 100644 --- a/src/app/components/buttons/DispatcherButton.js +++ b/src/app/components/buttons/DispatcherButton.js @@ -1,12 +1,15 @@ import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; import DispatchIcon from 'react-icons/lib/md/keyboard'; import DispatchHideIcon from 'react-icons/lib/md/keyboard-hide'; import Button from '../Button'; +import { toggleDispatcher } from '../../actions'; -export default class DispatcherButton extends Component { +class DispatcherButton extends Component { static propTypes = { dispatcherIsOpen: PropTypes.bool, - onClick: PropTypes.func.isRequired + toggleDispatcher: PropTypes.func.isRequired }; shouldComponentUpdate(nextProps) { @@ -17,8 +20,16 @@ export default class DispatcherButton extends Component { return ( ); } } + +function mapDispatchToProps(dispatch) { + return { + toggleDispatcher: bindActionCreators(toggleDispatcher, dispatch) + }; +} + +export default connect(null, mapDispatchToProps)(DispatcherButton); diff --git a/src/app/components/buttons/ExportButton.js b/src/app/components/buttons/ExportButton.js index c6737c0..1d949d7 100644 --- a/src/app/components/buttons/ExportButton.js +++ b/src/app/components/buttons/ExportButton.js @@ -5,7 +5,7 @@ import Button from '../Button'; export default class ExportButton extends Component { static propTypes = { - exportState: PropTypes.func.isRequired + liftedState: PropTypes.object.isRequired }; constructor() { @@ -19,9 +19,7 @@ export default class ExportButton extends Component { } handleExport() { - let state = this.props.exportState(); - if (!state) return; - state = encodeURIComponent(stringify(state)); + const state = encodeURIComponent(stringify(this.props.liftedState)); this.setState({ href: 'data:text/json;charset=utf-8,' + state }); } diff --git a/src/app/components/buttons/ImportButton.js b/src/app/components/buttons/ImportButton.js index ca1c902..8d1843f 100644 --- a/src/app/components/buttons/ImportButton.js +++ b/src/app/components/buttons/ImportButton.js @@ -1,11 +1,15 @@ import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; import { parse } from 'jsan'; import UploadIcon from 'react-icons/lib/md/file-upload'; import Button from '../Button'; +import { importState, showNotification } from '../../actions'; -export default class ImportButton extends Component { +class ImportButton extends Component { static propTypes = { - importState: PropTypes.func.isRequired + importState: PropTypes.func.isRequired, + showNotification: PropTypes.func.isRequired }; constructor() { @@ -36,10 +40,7 @@ export default class ImportButton extends Component { parse(state); // Check if it is in JSON format this.props.importState(state); } catch (error) { - // FIXME: add error notification - /* eslint-disable no-alert */ - alert('Invalid file'); - /* eslint-enable */ + this.props.showNotification('Invalid file'); } }; reader.readAsText(file); @@ -58,3 +59,12 @@ export default class ImportButton extends Component { ); } } + +function mapDispatchToProps(dispatch) { + return { + importState: bindActionCreators(importState, dispatch), + showNotification: bindActionCreators(showNotification, dispatch) + }; +} + +export default connect(null, mapDispatchToProps)(ImportButton); diff --git a/src/app/components/buttons/SliderButton.js b/src/app/components/buttons/SliderButton.js index 3586d4b..60b7f77 100644 --- a/src/app/components/buttons/SliderButton.js +++ b/src/app/components/buttons/SliderButton.js @@ -1,12 +1,15 @@ import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; import TimerIcon from 'react-icons/lib/md/timer'; import TimerOffIcon from 'react-icons/lib/md/timer-off'; import Button from '../Button'; +import { toggleSlider } from '../../actions'; -export default class DispatcherButton extends Component { +class SliderButton extends Component { static propTypes = { isOpen: PropTypes.bool, - onClick: PropTypes.func.isRequired + toggleSlider: PropTypes.func.isRequired }; shouldComponentUpdate(nextProps) { @@ -17,8 +20,16 @@ export default class DispatcherButton extends Component { return ( ); } } + +function mapDispatchToProps(dispatch) { + return { + toggleSlider: bindActionCreators(toggleSlider, dispatch) + }; +} + +export default connect(null, mapDispatchToProps)(SliderButton); diff --git a/src/app/components/reports/Table.js b/src/app/components/reports/Table.js new file mode 100644 index 0000000..c1b7f10 --- /dev/null +++ b/src/app/components/reports/Table.js @@ -0,0 +1,54 @@ +import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { + Table, TableBody, TableRow, TableRowColumn +} from 'material-ui/Table'; +import { getReport } from '../../actions'; + +const style = { + error: { color: '#EF5350' }, + date: { textAlign: 'right', maxWidth: '150px' } +}; + +class Reports extends Component { + onSelect = r => { + const idx = r[0]; + if (typeof idx === 'undefined') return; + this.props.getReport(this.props.reports[idx]); + }; + + render() { + return ( + + + {this.props.reports.map((data) => ( + + {data.title} + {data.added} + + ))} + +
+ ); + } +} + +Reports.propTypes = { + reports: PropTypes.array.isRequired, + getReport: PropTypes.func.isRequired +}; + +function mapStateToProps(state) { + return { + reports: state.reports.data + }; +} + +function mapDispatchToProps(dispatch) { + return { + getReport: bindActionCreators(getReport, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(Reports); diff --git a/src/app/constants/actionTypes.js b/src/app/constants/actionTypes.js new file mode 100644 index 0000000..91d41ea --- /dev/null +++ b/src/app/constants/actionTypes.js @@ -0,0 +1,23 @@ +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 SELECT_MONITOR = 'devTools/SELECT_MONITOR'; +export const TOGGLE_SLIDER = 'devTools/TOGGLE_SLIDER'; +export const TOGGLE_DISPATCHER = 'devTools/TOGGLE_DISPATCHER'; +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'; + +export const TEST_ADD = 'test/ADD'; +export const TEST_EDIT = 'test/EDIT'; +export const TEST_REMOVE = 'test/REMOVE'; +export const TEST_SELECT = 'test/SELECT'; diff --git a/src/app/constants/socketActionTypes.js b/src/app/constants/socketActionTypes.js new file mode 100644 index 0000000..1df2861 --- /dev/null +++ b/src/app/constants/socketActionTypes.js @@ -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'; diff --git a/src/app/constants/socketOptions.js b/src/app/constants/socketOptions.js index f0636f6..688e727 100644 --- a/src/app/constants/socketOptions.js +++ b/src/app/constants/socketOptions.js @@ -2,7 +2,10 @@ const socketOptions = { hostname: 'remotedev.io', port: 80, autoReconnect: true, - secure: false + secure: false, + autoReconnectOptions: { + randomness: 30000 + } }; export default socketOptions; diff --git a/src/app/containers/App.js b/src/app/containers/App.js index e2a0259..ae3ab33 100644 --- a/src/app/containers/App.js +++ b/src/app/containers/App.js @@ -1,194 +1,87 @@ import React, { Component, PropTypes } from 'react'; -import { - saveObjToStorage, getSettings, getFromStorage, saveToStorage -} from '../utils/localStorage'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { liftedDispatch } from '../actions'; +import { getActiveInstance } from '../reducers/instances'; import styles from '../styles'; import DevTools from '../containers/DevTools'; import Dispatcher from './monitors/Dispatcher'; -import { - createRemoteStore, updateStoreInstance, enableSync, - startMonitoring, importState, exportState -} from '../store/createRemoteStore'; -import { addInstance, deleteInstance } from '../services/messaging'; import ButtonBar from '../components/ButtonBar'; +import Notification from '../components/Notification'; import Instances from '../components/Instances'; import MonitorSelector from '../components/MonitorSelector'; import SyncToggle from '../components/SyncToggle'; import TestGenerator from '../components/TestGenerator'; -export default class App extends Component { +class App extends Component { static propTypes = { - selectMonitor: PropTypes.string, - testTemplates: PropTypes.array, - useCodemirror: PropTypes.bool, - selectedTemplate: PropTypes.number, - socketOptions: PropTypes.shape({ - hostname: PropTypes.string, - port: PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]), - autoReconnect: PropTypes.bool, - secure: PropTypes.bool - }), + liftedDispatch: PropTypes.func.isRequired, + selected: PropTypes.string, + liftedState: PropTypes.object.isRequired, + options: PropTypes.object.isRequired, + monitor: PropTypes.string, + dispatcherIsOpen: PropTypes.bool, + sliderIsOpen: PropTypes.bool, + shouldSync: PropTypes.bool, noSettings: PropTypes.bool }; - constructor() { - super(); - this.openModal = this.openModal.bind(this); - this.closeModal = this.closeModal.bind(this); - this.toggleDispatcher = this.toggleDispatcher.bind(this); - this.toggleSlider = this.toggleSlider.bind(this); - this.saveSettings = this.saveSettings.bind(this); - this.clearError = this.clearError.bind(this); - this.handleError = this.handleError.bind(this); - } - - componentWillMount() { - this.state = { - monitor: getFromStorage('select-monitor') || this.props.selectMonitor || 'default', - modalIsOpen: false, - dispatcherIsOpen: false, - sliderIsOpen: true, - instances: {}, - instance: null, - error: null, - shouldSync: false - }; - this.socketOptions = getSettings() || this.props.socketOptions; - this.store = this.createStore(); - this.testComponent = (props) => ( - - ); - } - - handleInstancesChanged = ({ id, instanceId }, name, toRemove) => { - const instances = this.state.instances; - if (toRemove) { - deleteInstance(id, (instance) => { - delete instances[instance]; - this.store.liftedStore.deleteInstance(instance); - if (this.state.instance === instance) { - updateStoreInstance(null); - this.setState({ instance: null, shouldSync: false }); - } - }); - } else { - addInstance(id, instanceId); - instances[instanceId] = name || instanceId; - startMonitoring(instanceId); - } - this.setState({ instances }); - }; - - handleSelectInstance = (event, index, instance) => { - updateStoreInstance(instance); - this.setState({ instance, shouldSync: false }); - }; - - handleSelectMonitor = (event, index, value) => { - this.setState({ monitor: saveToStorage('select-monitor', value) }); - }; - - handleSyncToggle = () => { - const shouldSync = !this.state.shouldSync; - enableSync(shouldSync); - this.setState({ shouldSync }); - }; - - createStore() { - return createRemoteStore( - this.socketOptions, - this.handleInstancesChanged, - this.handleError, - this.state.instance - ); - } - - saveSettings(isLocal, options) { - this.socketOptions = saveObjToStorage( - !isLocal, ['hostname', 'port', 'secure'], options - ) || undefined; - this.store = this.createStore(); - this.closeModal(); - } - - toggleDispatcher() { - this.setState({ dispatcherIsOpen: !this.state.dispatcherIsOpen }); - } - - handleError(error) { - this.setState({ error }); - } - - clearError() { - this.setState({ error: null }); - } - - toggleSlider() { - this.setState({ sliderIsOpen: !this.state.sliderIsOpen }); - } - - openModal(content) { - this.modalContent = content; - this.setState({ modal: this.modal, modalIsOpen: true }); - } - closeModal() { - this.modalContent = null; - this.setState({ modalIsOpen: false }); - } - render() { - const { monitor } = this.state; - const key = (this.socketOptions ? this.socketOptions.hostname : '') + this.state.instance; + const { monitor, dispatcherIsOpen, sliderIsOpen, options, liftedState } = this.props; return (
- - + +
- {this.state.sliderIsOpen &&
- -
} - {this.state.dispatcherIsOpen && this.store.liftedStore.getInstance() && - + {sliderIsOpen &&
+ +
} + {dispatcherIsOpen && options.connectionId && + }
); } } + +function mapStateToProps(state) { + const instances = state.instances; + const id = getActiveInstance(instances); + return { + selected: instances.selected, + liftedState: instances.states[id], + options: instances.options[id], + monitor: state.monitor.selected, + dispatcherIsOpen: state.monitor.dispatcherIsOpen, + sliderIsOpen: state.monitor.sliderIsOpen, + shouldSync: state.instances.sync + }; +} + +function mapDispatchToProps(dispatch) { + return { liftedDispatch: bindActionCreators(liftedDispatch, dispatch) }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(App); diff --git a/src/app/containers/DevTools.js b/src/app/containers/DevTools.js index 6347585..1adc20d 100644 --- a/src/app/containers/DevTools.js +++ b/src/app/containers/DevTools.js @@ -1,57 +1,43 @@ import React, { Component, PropTypes, createElement } from 'react'; -import { createDevTools } from 'redux-devtools'; -import LogMonitor from 'redux-devtools-log-monitor'; -import SliderMonitor from 'redux-slider-monitor'; -import InspectorMonitor from 'redux-devtools-inspector'; -import ChartMonitor from 'redux-devtools-chart-monitor'; +import getMonitor from './getMonitor'; -export const sideMonitors = [ - { key: 'LogMonitor', title: 'Log monitor' }, - { key: 'InspectorMonitor', title: 'Inspector' }, - { key: 'ChartMonitor', title: 'Chart' } -]; - -function getMonitor(type, props) { - switch (type) { - case 'LogMonitor': - return createElement(LogMonitor, { preserveScrollTop: false }); - case 'SliderMonitor': - return createElement(SliderMonitor); - case 'DispatchMonitor': - return createElement('div'); // deprecated - case 'ChartMonitor': - return createElement(ChartMonitor, { - defaultIsVisible: true, invertTheme: true, - tooltipOptions: { - style: { - 'background-color': '#ffffff', - 'color': '#000000', - 'opacity': '0.9', - 'border-radius': '5px', - 'padding': '5px' - } - } - }); - default: - return createElement(InspectorMonitor, { - shouldPersistState: false, isLightTheme: false, theme: 'nicinabox', - tabs: defaultTabs => [...defaultTabs, { name: 'Test', component: props.testComponent }] - }); +export default class DevTools extends Component { + constructor(props) { + super(props); + this.monitorElement = getMonitor(props); } -} -export default class extends Component { - static propTypes = { - monitor: PropTypes.string - }; + componentWillReceiveProps(nextProps) { + if ( + nextProps.monitor !== this.props.monitor || + nextProps.testComponent !== this.props.testComponent + ) { + this.monitorElement = getMonitor(nextProps); + } + } shouldComponentUpdate(nextProps) { - return nextProps.monitor !== this.props.monitor; + return ( + nextProps.monitor !== this.props.monitor || + nextProps.liftedState !== this.props.liftedState || + nextProps.testComponent !== this.props.testComponent + ); } render() { - const { monitor, ...rest } = this.props; - const DevTools = createDevTools(getMonitor(monitor, rest)); - return ; + const { liftedState, dispatch } = this.props; + const monitorProps = this.monitorElement.props; + const Monitor = this.monitorElement.type; + return ; } } + +DevTools.propTypes = { + liftedState: PropTypes.object, + dispatch: PropTypes.func.isRequired, + monitor: PropTypes.string, + testComponent: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.func + ]) +}; diff --git a/src/app/containers/Layout.js b/src/app/containers/Layout.js new file mode 100644 index 0000000..4d4d0d5 --- /dev/null +++ b/src/app/containers/Layout.js @@ -0,0 +1,39 @@ +import React, { Component, PropTypes, cloneElement } from 'react'; +import { withRouter } from 'react-router'; +import { Tabs, Tab } from 'material-ui/Tabs'; +import enhance from '../hoc'; +import styles from '../styles'; + +export default class Layout extends Component { + constructor(props) { + super(props); + this.state = { tab: 0 }; + } + handleChange = (pathname) => { + this.props.router.push(pathname); + }; + render() { + const { children, ...rest } = this.props; + return ( +
+ + + + + {cloneElement(children, rest)} +
+ ); + } +} + +Layout.propTypes = { + children: PropTypes.any, + router: PropTypes.object, + location: PropTypes.object +}; + +export default enhance(withRouter(Layout)); diff --git a/src/app/containers/getMonitor.js b/src/app/containers/getMonitor.js new file mode 100644 index 0000000..7c2bff0 --- /dev/null +++ b/src/app/containers/getMonitor.js @@ -0,0 +1,41 @@ +import React, { Component, PropTypes, createElement } from 'react'; +import LogMonitor from 'redux-devtools-log-monitor'; +import SliderMonitor from 'redux-slider-monitor'; +import InspectorMonitor from 'redux-devtools-inspector'; +import ChartMonitor from 'redux-devtools-chart-monitor'; + +export const monitors = [ + { key: 'LogMonitor', title: 'Log monitor' }, + { key: 'InspectorMonitor', title: 'Inspector' }, + { key: 'ChartMonitor', title: 'Chart' } +]; + +export default function getMonitor({ monitor, testComponent }) { + switch (monitor) { + case 'LogMonitor': + return ; + case 'SliderMonitor': + return ; + case 'ChartMonitor': + return createElement(ChartMonitor, { + defaultIsVisible: true, invertTheme: true, + tooltipOptions: { + style: { + 'background-color': '#ffffff', + 'color': '#000000', + 'opacity': '0.9', + 'border-radius': '5px', + 'padding': '5px' + } + } + }); + default: + let tabs; + if (testComponent) { + tabs = defaultTabs => [...defaultTabs, { name: 'Test', component: testComponent }]; + } + return createElement(InspectorMonitor, { + shouldPersistState: false, isLightTheme: false, theme: 'nicinabox', tabs + }); + } +} diff --git a/src/app/containers/monitors/Dispatcher.js b/src/app/containers/monitors/Dispatcher.js index fb36551..075203a 100644 --- a/src/app/containers/monitors/Dispatcher.js +++ b/src/app/containers/monitors/Dispatcher.js @@ -2,6 +2,9 @@ import React, { Component, PropTypes } from 'react'; import * as themes from 'redux-devtools-themes'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { dispatchRemotely } from '../../actions'; const styles = { button: { @@ -33,15 +36,14 @@ const styles = { } }; -export default class Dispatcher extends Component { +class Dispatcher extends Component { static propTypes = { - store: PropTypes.object.isRequired, + options: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, theme: PropTypes.oneOfType([ PropTypes.object, PropTypes.string, - ]), - error: PropTypes.string, - clearError: PropTypes.func.isRequired + ]) }; static defaultProps = { @@ -62,7 +64,7 @@ export default class Dispatcher extends Component { if (selected !== 'default') { // Shrink the number args to the number of the new ones args = this.state.args.slice( - 0, this.props.store.getActionCreators()[selected].args.length + 0, this.props.options.actionCreators[selected].args.length ); } this.setState({ @@ -87,16 +89,15 @@ export default class Dispatcher extends Component { let rest = this.refs.restArgs.textContent.trim(); if (rest === '') rest = undefined; const { selected, args } = this.state; - this.props.store.dispatch({ - name: this.props.store.getActionCreators()[selected].name, + this.props.dispatch({ + name: this.props.options.actionCreators[selected].name, selected, args, rest }); } else { if (this.refs.action.textContent !== '') { - this.props.store.dispatch(this.refs.action.textContent); + this.props.dispatch(this.refs.action.textContent); } } - this.props.clearError(); } componentDidMount() { @@ -113,7 +114,7 @@ export default class Dispatcher extends Component { } componentWillReceiveProps(nextProps) { - if (this.state.selected !== 'default' && !nextProps.store.getActionCreators()) { + if (this.state.selected !== 'default' && !nextProps.options.actionCreators) { this.setState({ selected: 'default', args: [] @@ -122,7 +123,9 @@ export default class Dispatcher extends Component { } resetCustomAction() { - this.refs.action.innerHTML = this.props.store.isRedux() ? '{
type: \'\'
}' : 'this.'; + this.refs.action.innerHTML = ( + this.props.options.lib === 'redux' ? '{
type: \'\'
}' : 'this.' + ); } getTheme() { @@ -147,7 +150,7 @@ export default class Dispatcher extends Component { const buttonStyle = { ...styles.button, color: theme.base06, backgroundColor: theme.base00 }; - const actionCreators = this.props.store.getActionCreators(); + const actionCreators = this.props.options.actionCreators; let fields =
; if (this.state.selected !== 'default' && actionCreators) { @@ -169,23 +172,6 @@ export default class Dispatcher extends Component { ); } - let error; - if (this.props.error) { - error = ( -
-
-

{this.props.error}

-
-
- -
-
- ); - } - let dispatchButtonStyle = buttonStyle; if (!actionCreators || actionCreators.length <= 0) { dispatchButtonStyle = { @@ -209,7 +195,6 @@ export default class Dispatcher extends Component { position: 'relative' }} > - {error} {fields} {actionCreators && actionCreators.length > 0 ?