From 6f2503a09f2c9b119175f81b05cd0e3ef942922a Mon Sep 17 00:00:00 2001 From: Ildar Kamalov Date: Tue, 28 May 2019 15:07:46 +0300 Subject: [PATCH] + client: handle static leases form --- client/src/__locales/en.json | 7 + client/src/actions/index.js | 123 +++++++++--------- client/src/api/Api.js | 20 +++ client/src/components/Settings/Dhcp/Form.js | 94 ++++++++++++- .../src/components/Settings/Dhcp/Interface.js | 114 ---------------- .../components/Settings/Dhcp/StaticLeases.js | 49 ------- .../Settings/Dhcp/StaticLeases/Form.js | 97 ++++++++++++++ .../Settings/Dhcp/StaticLeases/Modal.js | 49 +++++++ .../Settings/Dhcp/StaticLeases/index.js | 111 ++++++++++++++++ client/src/components/Settings/Dhcp/index.js | 51 +++++--- client/src/components/Settings/index.js | 3 + client/src/containers/Settings.js | 6 + client/src/reducers/index.js | 31 +++++ 13 files changed, 510 insertions(+), 245 deletions(-) delete mode 100644 client/src/components/Settings/Dhcp/Interface.js delete mode 100644 client/src/components/Settings/Dhcp/StaticLeases.js create mode 100644 client/src/components/Settings/Dhcp/StaticLeases/Form.js create mode 100644 client/src/components/Settings/Dhcp/StaticLeases/Modal.js create mode 100644 client/src/components/Settings/Dhcp/StaticLeases/index.js diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 465a4006..697dbbcd 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -38,6 +38,13 @@ "dhcp_error": "We could not determine whether there is another DHCP server in the network.", "dhcp_static_ip_error": "In order to use DHCP server a static IP address must be set. We failed to determine if this network interface is configured using static IP address. Please set a static IP address manually.", "dhcp_dynamic_ip_found": "Your system uses dynamic IP address configuration for interface <0>{{interfaceName}}. In order to use DHCP server a static IP address must be set. Your current IP address is <0>{{ipAddress}}. We will automatically set this IP address as static if you press Enable DHCP button.", + "dhcp_lease_added": "Static lease \"{{key}}\" successfully added", + "dhcp_lease_deleted": "Static lease \"{{key}}\" successfully deleted", + "dhcp_new_static_lease": "New static lease", + "dhcp_static_leases_not_found": "No DHCP static leases found", + "dhcp_add_static_lease": "Add static lease", + "delete_confirm": "Are you sure you want to delete \"{{key}}\"?", + "form_enter_hostname": "Enter hostname", "error_details": "Error details", "back": "Back", "dashboard": "Dashboard", diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 39224388..a67aa987 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -662,41 +662,18 @@ export const setDhcpConfigRequest = createAction('SET_DHCP_CONFIG_REQUEST'); export const setDhcpConfigSuccess = createAction('SET_DHCP_CONFIG_SUCCESS'); export const setDhcpConfigFailure = createAction('SET_DHCP_CONFIG_FAILURE'); -// TODO rewrite findActiveDhcp part export const setDhcpConfig = values => async (dispatch, getState) => { const { config } = getState().dhcp; const updatedConfig = { ...config, ...values }; dispatch(setDhcpConfigRequest()); - if (values.interface_name) { - dispatch(findActiveDhcpRequest()); - try { - const activeDhcp = await apiClient.findActiveDhcp(values.interface_name); - dispatch(findActiveDhcpSuccess(activeDhcp)); - if (!activeDhcp.found) { - try { - await apiClient.setDhcpConfig(updatedConfig); - dispatch(setDhcpConfigSuccess(updatedConfig)); - dispatch(addSuccessToast('dhcp_config_saved')); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(setDhcpConfigFailure()); - } - } else { - dispatch(addErrorToast({ error: 'dhcp_found' })); - } - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(findActiveDhcpFailure()); - } - } else { - try { - await apiClient.setDhcpConfig(updatedConfig); - dispatch(setDhcpConfigSuccess(updatedConfig)); - dispatch(addSuccessToast('dhcp_config_saved')); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(setDhcpConfigFailure()); - } + dispatch(findActiveDhcp(values.interface_name)); + try { + await apiClient.setDhcpConfig(updatedConfig); + dispatch(setDhcpConfigSuccess(updatedConfig)); + dispatch(addSuccessToast('dhcp_config_saved')); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(setDhcpConfigFailure()); } }; @@ -704,40 +681,60 @@ export const toggleDhcpRequest = createAction('TOGGLE_DHCP_REQUEST'); export const toggleDhcpFailure = createAction('TOGGLE_DHCP_FAILURE'); export const toggleDhcpSuccess = createAction('TOGGLE_DHCP_SUCCESS'); -// TODO rewrite findActiveDhcp part -export const toggleDhcp = config => async (dispatch) => { +export const toggleDhcp = values => async (dispatch) => { dispatch(toggleDhcpRequest()); + let config = { ...values, enabled: false }; + let successMessage = 'disabled_dhcp'; - if (config.enabled) { - try { - await apiClient.setDhcpConfig({ ...config, enabled: false }); - dispatch(toggleDhcpSuccess()); - dispatch(addSuccessToast('disabled_dhcp')); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(toggleDhcpFailure()); - } - } else { - dispatch(findActiveDhcpRequest()); - try { - const activeDhcp = await apiClient.findActiveDhcp(config.interface_name); - dispatch(findActiveDhcpSuccess(activeDhcp)); + if (!values.enabled) { + config = { ...values, enabled: true }; + successMessage = 'enabled_dhcp'; + dispatch(findActiveDhcp(values.interface_name)); + } - if (!activeDhcp.found) { - try { - await apiClient.setDhcpConfig({ ...config, enabled: true }); - dispatch(toggleDhcpSuccess()); - dispatch(addSuccessToast('enabled_dhcp')); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(toggleDhcpFailure()); - } - } else { - dispatch(addErrorToast({ error: 'dhcp_found' })); - } - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(findActiveDhcpFailure()); - } + try { + await apiClient.setDhcpConfig(config); + dispatch(toggleDhcpSuccess()); + dispatch(addSuccessToast(successMessage)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(toggleDhcpFailure()); + } +}; + +export const toggleLeaseModal = createAction('TOGGLE_LEASE_MODAL'); + +export const addStaticLeaseRequest = createAction('ADD_STATIC_LEASE_REQUEST'); +export const addStaticLeaseFailure = createAction('ADD_STATIC_LEASE_FAILURE'); +export const addStaticLeaseSuccess = createAction('ADD_STATIC_LEASE_SUCCESS'); + +export const addStaticLease = config => async (dispatch) => { + dispatch(addStaticLeaseRequest()); + try { + await apiClient.addStaticLease(config); + dispatch(addStaticLeaseSuccess()); + dispatch(addSuccessToast(t('dhcp_lease_added', { key: config.hostname }))); + dispatch(toggleLeaseModal()); + dispatch(getDhcpStatus()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(addStaticLeaseFailure()); + } +}; + +export const removeStaticLeaseRequest = createAction('REMOVE_STATIC_LEASE_REQUEST'); +export const removeStaticLeaseFailure = createAction('REMOVE_STATIC_LEASE_FAILURE'); +export const removeStaticLeaseSuccess = createAction('REMOVE_STATIC_LEASE_SUCCESS'); + +export const removeStaticLease = config => async (dispatch) => { + dispatch(removeStaticLeaseRequest()); + try { + await apiClient.removeStaticLease(config); + dispatch(removeStaticLeaseSuccess()); + dispatch(addSuccessToast(t('dhcp_lease_deleted', { key: config.hostname }))); + dispatch(getDhcpStatus()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(removeStaticLeaseFailure()); } }; diff --git a/client/src/api/Api.js b/client/src/api/Api.js index 8f34f201..81bce7cf 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -318,6 +318,8 @@ export default class Api { DHCP_SET_CONFIG = { path: 'dhcp/set_config', method: 'POST' }; DHCP_FIND_ACTIVE = { path: 'dhcp/find_active_dhcp', method: 'POST' }; DHCP_INTERFACES = { path: 'dhcp/interfaces', method: 'GET' }; + DHCP_ADD_STATIC_LEASE = { path: 'dhcp/add_static_lease', method: 'POST' }; + DHCP_REMOVE_STATIC_LEASE = { path: 'dhcp/remove_static_lease', method: 'POST' }; getDhcpStatus() { const { path, method } = this.DHCP_STATUS; @@ -347,6 +349,24 @@ export default class Api { return this.makeRequest(path, method, parameters); } + addStaticLease(config) { + const { path, method } = this.DHCP_ADD_STATIC_LEASE; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, parameters); + } + + removeStaticLease(config) { + const { path, method } = this.DHCP_REMOVE_STATIC_LEASE; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, parameters); + } + // Installation INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' }; INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' }; diff --git a/client/src/components/Settings/Dhcp/Form.js b/client/src/components/Settings/Dhcp/Form.js index af7c1931..86de6340 100644 --- a/client/src/components/Settings/Dhcp/Form.js +++ b/client/src/components/Settings/Dhcp/Form.js @@ -1,22 +1,97 @@ import React from 'react'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { Field, reduxForm } from 'redux-form'; -import { withNamespaces } from 'react-i18next'; +import { Field, reduxForm, formValueSelector } from 'redux-form'; +import { Trans, withNamespaces } from 'react-i18next'; import flow from 'lodash/flow'; import { renderField, required, ipv4, isPositive, toNumber } from '../../../helpers/form'; -const Form = (props) => { +const renderInterfaces = (interfaces => ( + Object.keys(interfaces).map((item) => { + const option = interfaces[item]; + const { name } = option; + const onlyIPv6 = option.ip_addresses.every(ip => ip.includes(':')); + let interfaceIP = option.ip_addresses[0]; + + if (!onlyIPv6) { + option.ip_addresses.forEach((ip) => { + if (!ip.includes(':')) { + interfaceIP = ip; + } + }); + } + + return ( + + ); + }) +)); + +const renderInterfaceValues = (interfaceValues => ( + +)); + +let Form = (props) => { const { t, handleSubmit, submitting, invalid, + enabled, + interfaces, + interfaceValue, processingConfig, + processingInterfaces, } = props; return (
+ {!processingInterfaces && interfaces && +
+
+
+ + + + {renderInterfaces(interfaces)} + +
+
+ {interfaceValue && +
+ {interfaces[interfaceValue] && + renderInterfaceValues(interfaces[interfaceValue])} +
+ } +
+ } +
@@ -101,11 +176,24 @@ Form.propTypes = { submitting: PropTypes.bool, invalid: PropTypes.bool, interfaces: PropTypes.object, + interfaceValue: PropTypes.string, initialValues: PropTypes.object, processingConfig: PropTypes.bool, + processingInterfaces: PropTypes.bool, + enabled: PropTypes.bool, t: PropTypes.func, }; + +const selector = formValueSelector('dhcpForm'); + +Form = connect((state) => { + const interfaceValue = selector(state, 'interface_name'); + return { + interfaceValue, + }; +})(Form); + export default flow([ withNamespaces(), reduxForm({ form: 'dhcpForm' }), diff --git a/client/src/components/Settings/Dhcp/Interface.js b/client/src/components/Settings/Dhcp/Interface.js deleted file mode 100644 index 3b9d3e03..00000000 --- a/client/src/components/Settings/Dhcp/Interface.js +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import { Field, reduxForm, formValueSelector } from 'redux-form'; -import { withNamespaces, Trans } from 'react-i18next'; -import flow from 'lodash/flow'; - -const renderInterfaces = (interfaces => ( - Object.keys(interfaces).map((item) => { - const option = interfaces[item]; - const { name } = option; - const onlyIPv6 = option.ip_addresses.every(ip => ip.includes(':')); - let interfaceIP = option.ip_addresses[0]; - - if (!onlyIPv6) { - option.ip_addresses.forEach((ip) => { - if (!ip.includes(':')) { - interfaceIP = ip; - } - }); - } - - return ( - - ); - }) -)); - -const renderInterfaceValues = (interfaceValues => ( -
    -
  • - MTU: - {interfaceValues.mtu} -
  • -
  • - dhcp_hardware_address: - {interfaceValues.hardware_address} -
  • -
  • - dhcp_ip_addresses: - { - interfaceValues.ip_addresses - .map(ip => {ip}) - } -
  • -
-)); - -let Interface = (props) => { - const { - t, - handleChange, - interfaces, - processing, - interfaceValue, - enabled, - } = props; - - return ( - - {!processing && interfaces && -
-
-
- - - - {renderInterfaces(interfaces)} - -
-
- {interfaceValue && -
- {interfaces[interfaceValue] && - renderInterfaceValues(interfaces[interfaceValue])} -
- } -
- } -
- - ); -}; - -Interface.propTypes = { - handleChange: PropTypes.func, - interfaces: PropTypes.object, - processing: PropTypes.bool, - interfaceValue: PropTypes.string, - initialValues: PropTypes.object, - enabled: PropTypes.bool, - t: PropTypes.func, -}; - -const selector = formValueSelector('dhcpInterface'); - -Interface = connect((state) => { - const interfaceValue = selector(state, 'interface_name'); - return { - interfaceValue, - }; -})(Interface); - -export default flow([ - withNamespaces(), - reduxForm({ form: 'dhcpInterface' }), -])(Interface); diff --git a/client/src/components/Settings/Dhcp/StaticLeases.js b/client/src/components/Settings/Dhcp/StaticLeases.js deleted file mode 100644 index 6bcf596c..00000000 --- a/client/src/components/Settings/Dhcp/StaticLeases.js +++ /dev/null @@ -1,49 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import ReactTable from 'react-table'; -import { Trans, withNamespaces } from 'react-i18next'; - -class StaticLeases extends Component { - cellWrap = ({ value }) => ( -
- - {value} - -
- ); - - render() { - const { staticLeases, t } = this.props; - return ( - dhcp_table_hostname, - accessor: 'hostname', - Cell: this.cellWrap, - }, - ]} - showPagination={false} - noDataText={t('dhcp_leases_not_found')} - minRows={6} - className="-striped -highlight card-table-overflow" - /> - ); - } -} - -StaticLeases.propTypes = { - staticLeases: PropTypes.array, - t: PropTypes.func, -}; - -export default withNamespaces()(StaticLeases); diff --git a/client/src/components/Settings/Dhcp/StaticLeases/Form.js b/client/src/components/Settings/Dhcp/StaticLeases/Form.js new file mode 100644 index 00000000..28f843ed --- /dev/null +++ b/client/src/components/Settings/Dhcp/StaticLeases/Form.js @@ -0,0 +1,97 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Field, reduxForm } from 'redux-form'; +import { Trans, withNamespaces } from 'react-i18next'; +import flow from 'lodash/flow'; + +import { renderField, ipv4, mac, required } from '../../../../helpers/form'; + +const Form = (props) => { + const { + t, + handleSubmit, + reset, + pristine, + submitting, + toggleLeaseModal, + processingAdding, + } = props; + + return ( +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ ); +}; + +Form.propTypes = { + pristine: PropTypes.bool.isRequired, + handleSubmit: PropTypes.func.isRequired, + reset: PropTypes.func.isRequired, + submitting: PropTypes.bool.isRequired, + toggleLeaseModal: PropTypes.func.isRequired, + processingAdding: PropTypes.bool.isRequired, + t: PropTypes.func.isRequired, +}; + +export default flow([ + withNamespaces(), + reduxForm({ form: 'leaseForm' }), +])(Form); diff --git a/client/src/components/Settings/Dhcp/StaticLeases/Modal.js b/client/src/components/Settings/Dhcp/StaticLeases/Modal.js new file mode 100644 index 00000000..6291f274 --- /dev/null +++ b/client/src/components/Settings/Dhcp/StaticLeases/Modal.js @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Trans, withNamespaces } from 'react-i18next'; +import ReactModal from 'react-modal'; + +import Form from './Form'; + +const Modal = (props) => { + const { + isModalOpen, + handleSubmit, + toggleLeaseModal, + processingAdding, + } = props; + + return ( + toggleLeaseModal()} + > +
+
+

+ dhcp_new_static_lease +

+ +
+
+
+
+ ); +}; + +Modal.propTypes = { + isModalOpen: PropTypes.bool.isRequired, + handleSubmit: PropTypes.func.isRequired, + toggleLeaseModal: PropTypes.func.isRequired, + processingAdding: PropTypes.bool.isRequired, +}; + +export default withNamespaces()(Modal); diff --git a/client/src/components/Settings/Dhcp/StaticLeases/index.js b/client/src/components/Settings/Dhcp/StaticLeases/index.js new file mode 100644 index 00000000..1e5b9208 --- /dev/null +++ b/client/src/components/Settings/Dhcp/StaticLeases/index.js @@ -0,0 +1,111 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import ReactTable from 'react-table'; +import { Trans, withNamespaces } from 'react-i18next'; + +import Modal from './Modal'; + +class StaticLeases extends Component { + cellWrap = ({ value }) => ( +
+ + {value} + +
+ ); + + handleSubmit = (data) => { + this.props.addStaticLease(data); + } + + handleDelete = (ip, mac, hostname) => { + // eslint-disable-next-line no-alert + if (window.confirm(this.props.t('delete_confirm', { key: hostname }))) { + this.props.removeStaticLease({ ip, mac, hostname }); + } + } + + render() { + const { + isModalOpen, + toggleLeaseModal, + processingAdding, + processingDeleting, + staticLeases, + t, + } = this.props; + return ( + + dhcp_table_hostname, + accessor: 'hostname', + Cell: this.cellWrap, + }, + { + Header: actions_table_header, + accessor: 'actions', + maxWidth: 150, + Cell: (row) => { + const { ip, mac, hostname } = row.original; + + return ( +
+ +
+ ); + }, + }, + ]} + showPagination={false} + noDataText={t('dhcp_static_leases_not_found')} + className="-striped -highlight card-table-overflow" + minRows={6} + /> + +
+ ); + } +} + +StaticLeases.propTypes = { + staticLeases: PropTypes.array.isRequired, + isModalOpen: PropTypes.bool.isRequired, + toggleLeaseModal: PropTypes.func.isRequired, + removeStaticLease: PropTypes.func.isRequired, + addStaticLease: PropTypes.func.isRequired, + processingAdding: PropTypes.bool.isRequired, + processingDeleting: PropTypes.bool.isRequired, + t: PropTypes.func.isRequired, +}; + +export default withNamespaces()(StaticLeases); diff --git a/client/src/components/Settings/Dhcp/index.js b/client/src/components/Settings/Dhcp/index.js index b7b5ff53..d33f1bcf 100644 --- a/client/src/components/Settings/Dhcp/index.js +++ b/client/src/components/Settings/Dhcp/index.js @@ -6,14 +6,15 @@ import { Trans, withNamespaces } from 'react-i18next'; import { DHCP_STATUS_RESPONSE } from '../../../helpers/constants'; import Form from './Form'; import Leases from './Leases'; -import StaticLeases from './StaticLeases'; -import Interface from './Interface'; +import StaticLeases from './StaticLeases/index'; import Card from '../../ui/Card'; import Accordion from '../../ui/Accordion'; class Dhcp extends Component { handleFormSubmit = (values) => { - this.props.setDhcpConfig(values); + if (values.interface_name) { + this.props.setDhcpConfig(values); + } }; handleToggle = (config) => { @@ -169,18 +170,16 @@ class Dhcp extends Component {
{!dhcp.processing && -
@@ -189,11 +188,11 @@ class Dhcp extends Component { type="button" className={statusButtonClass} onClick={() => - this.props.findActiveDhcp(dhcp.config.interface_name) + this.props.findActiveDhcp(interface_name) } disabled={ - dhcp.config.enabled - || !dhcp.config.interface_name + enabled + || !interface_name || dhcp.processingConfig } > @@ -222,8 +221,25 @@ class Dhcp extends Component {
-
- +
+ +
+
+
@@ -241,6 +257,9 @@ Dhcp.propTypes = { setDhcpConfig: PropTypes.func, findActiveDhcp: PropTypes.func, handleSubmit: PropTypes.func, + addStaticLease: PropTypes.func, + removeStaticLease: PropTypes.func, + toggleLeaseModal: PropTypes.func, t: PropTypes.func, }; diff --git a/client/src/components/Settings/index.js b/client/src/components/Settings/index.js index 3435e610..8d36c6c4 100644 --- a/client/src/components/Settings/index.js +++ b/client/src/components/Settings/index.js @@ -128,6 +128,9 @@ class Settings extends Component { getDhcpStatus={this.props.getDhcpStatus} findActiveDhcp={this.props.findActiveDhcp} setDhcpConfig={this.props.setDhcpConfig} + addStaticLease={this.props.addStaticLease} + removeStaticLease={this.props.removeStaticLease} + toggleLeaseModal={this.props.toggleLeaseModal} />
diff --git a/client/src/containers/Settings.js b/client/src/containers/Settings.js index 95be768b..0255e044 100644 --- a/client/src/containers/Settings.js +++ b/client/src/containers/Settings.js @@ -11,6 +11,9 @@ import { getDhcpInterfaces, setDhcpConfig, findActiveDhcp, + addStaticLease, + removeStaticLease, + toggleLeaseModal, } from '../actions'; import { getTlsStatus, @@ -62,6 +65,9 @@ const mapDispatchToProps = { updateClient, deleteClient, toggleClientModal, + addStaticLease, + removeStaticLease, + toggleLeaseModal, }; export default connect( diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js index d8767116..67d9b737 100644 --- a/client/src/reducers/index.js +++ b/client/src/reducers/index.js @@ -351,18 +351,49 @@ const dhcp = handleActions({ const newState = { ...state, config: newConfig, processingConfig: false }; return newState; }, + + [actions.toggleLeaseModal]: (state) => { + const newState = { + ...state, + isModalOpen: !state.isModalOpen, + }; + return newState; + }, + + [actions.addStaticLeaseRequest]: state => ({ ...state, processingAdding: true }), + [actions.addStaticLeaseFailure]: state => ({ ...state, processingAdding: false }), + [actions.addStaticLeaseSuccess]: (state) => { + const newState = { + ...state, + processingAdding: false, + }; + return newState; + }, + + [actions.removeStaticLeaseRequest]: state => ({ ...state, processingDeleting: true }), + [actions.removeStaticLeaseFailure]: state => ({ ...state, processingDeleting: false }), + [actions.removeStaticLeaseSuccess]: (state) => { + const newState = { + ...state, + processingDeleting: false, + }; + return newState; + }, }, { processing: true, processingStatus: false, processingInterfaces: false, processingDhcp: false, processingConfig: false, + processingAdding: false, + processingDeleting: false, config: { enabled: false, }, check: null, leases: [], staticLeases: [], + isModalOpen: false, }); export default combineReducers({