You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@couchdb.apache.org by GitBox <gi...@apache.org> on 2018/10/15 18:38:36 UTC

[GitHub] Antonio-Maranhao closed pull request #1138: Update config addon to use redux

Antonio-Maranhao closed pull request #1138: Update config addon to use redux
URL: https://github.com/apache/couchdb-fauxton/pull/1138
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/app/addons/config/__tests__/actions.test.js b/app/addons/config/__tests__/actions.test.js
index 410dabcc0..d9e9649f9 100644
--- a/app/addons/config/__tests__/actions.test.js
+++ b/app/addons/config/__tests__/actions.test.js
@@ -9,13 +9,13 @@
 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 // License for the specific language governing permissions and limitations under
 // the License.
-import testUtils from "../../../../test/mocha/testUtils";
-import FauxtonAPI from "../../../core/api";
-import Actions from "../actions";
-import Backbone from "backbone";
-import sinon from "sinon";
+import testUtils from '../../../../test/mocha/testUtils';
+import FauxtonAPI from '../../../core/api';
+import * as Actions from '../actions';
+import ActionTypes from '../actiontypes';
+import * as ConfigAPI from '../api';
+import sinon from 'sinon';
 
-const assert = testUtils.assert;
 const restore = testUtils.restore;
 
 describe('Config Actions', () => {
@@ -25,190 +25,121 @@ describe('Config Actions', () => {
     optionName: 'test',
     value: 'test'
   };
-  const failXhr = { responseText: '{}' };
+  const spySaveConfigOption = sinon.stub(ConfigAPI, 'saveConfigOption');
+  const spyDeleteConfigOption = sinon.stub(ConfigAPI, 'deleteConfigOption');
+  const dispatch = sinon.stub();
+
+  describe('addOption', () => {
 
-  describe('add', () => {
     afterEach(() => {
-      restore(Actions.optionAddSuccess);
-      restore(Actions.optionAddFailure);
-      restore(FauxtonAPI.when);
+      spySaveConfigOption.reset();
+      dispatch.reset();
       restore(FauxtonAPI.addNotification);
-      restore(Backbone.Model.prototype.save);
-    });
-
-    it('calls optionAddSuccess when option add succeeds', () => {
-      const stub = sinon.stub(Backbone.Model.prototype, 'save');
-      const spy = sinon.spy(Actions, 'optionAddSuccess');
-      const promise = FauxtonAPI.Deferred();
-      promise.resolve();
-      stub.returns(promise);
-
-      return Actions.addOption(node, option)
-        .then(() => {
-          assert.ok(spy.calledOnce);
-        });
-    });
-
-    it('shows notification when option add succeeds', () => {
-      const stub = sinon.stub(Backbone.Model.prototype, 'save');
-      const spy = sinon.spy(FauxtonAPI, 'addNotification');
-      const promise = FauxtonAPI.Deferred();
-      promise.resolve();
-      stub.returns(promise);
-
-      return Actions.addOption(node, option)
-        .then(() => {
-          assert.ok(spy.calledOnce);
-        });
     });
 
-    it('calls optionAddFailure when option add fails', () => {
-      const stub = sinon.stub(Backbone.Model.prototype, 'save');
-      const spy = sinon.spy(Actions, 'optionAddFailure');
-      const promise = FauxtonAPI.Deferred();
-      promise.reject(failXhr);
-      stub.returns(promise);
+    it('dispatches OPTION_ADD_SUCCESS and shows notification when option add succeeds', () => {
+      const promise = FauxtonAPI.Promise.resolve();
+      spySaveConfigOption.returns(promise);
+      const spyAddNotification = sinon.spy(FauxtonAPI, 'addNotification');
 
-      return Actions.addOption(node, option)
+      return Actions.addOption(node, option)(dispatch)
         .then(() => {
-          assert.ok(spy.calledOnce);
+          sinon.assert.calledWith(dispatch, {
+            type: ActionTypes.OPTION_ADD_SUCCESS,
+            options: { optionName: "test", sectionName: "test", value: "test" }
+          });
+          sinon.assert.called(spyAddNotification);
         });
     });
 
-    it('shows notification when option add fails', () => {
-      const stub = sinon.stub(Backbone.Model.prototype, 'save');
-      const spy = sinon.spy(FauxtonAPI, 'addNotification');
-      const promise = FauxtonAPI.Deferred();
-      promise.reject(failXhr);
-      stub.returns(promise);
+    it('dispatches OPTION_ADD_FAILURE and shows notification when option add fails', () => {
+      const promise = FauxtonAPI.Promise.reject(new Error(''));
+      spySaveConfigOption.returns(promise);
+      const spyAddNotification = sinon.spy(FauxtonAPI, 'addNotification');
 
-      return Actions.addOption(node, option)
+      return Actions.addOption(node, option)(dispatch)
         .then(() => {
-          assert.ok(spy.calledOnce);
+          sinon.assert.calledWith(dispatch, {
+            type: ActionTypes.OPTION_ADD_FAILURE,
+            options: { optionName: "test", sectionName: "test", value: "test" }
+          });
+          sinon.assert.called(spyAddNotification);
         });
     });
   });
 
-  describe('save', () => {
+  describe('saveOption', () => {
     afterEach(() => {
-      restore(Actions.optionSaveSuccess);
-      restore(Actions.optionSaveFailure);
-      restore(FauxtonAPI.when);
+      spySaveConfigOption.reset();
+      dispatch.reset();
       restore(FauxtonAPI.addNotification);
-      restore(Backbone.Model.prototype.save);
-    });
-
-    it('calls optionSaveSuccess when option save succeeds', () => {
-      const stub = sinon.stub(Backbone.Model.prototype, 'save');
-      const spy = sinon.spy(Actions, 'optionSaveSuccess');
-      const promise = FauxtonAPI.Deferred();
-      promise.resolve();
-      stub.returns(promise);
-
-      return Actions.saveOption(node, option)
-        .then(() => {
-          assert.ok(spy.calledOnce);
-        });
     });
 
-    it('shows notification when option save succeeds', () => {
-      const stub = sinon.stub(Backbone.Model.prototype, 'save');
-      const spy = sinon.spy(FauxtonAPI, 'addNotification');
-      const promise = FauxtonAPI.Deferred();
-      promise.resolve();
-      stub.returns(promise);
+    it('dispatches OPTION_SAVE_SUCCESS and shows notification when option add succeeds', () => {
+      const promise = FauxtonAPI.Promise.resolve();
+      spySaveConfigOption.returns(promise);
+      const spyAddNotification = sinon.spy(FauxtonAPI, 'addNotification');
 
-      return Actions.saveOption(node, option)
+      return Actions.saveOption(node, option)(dispatch)
         .then(() => {
-          assert.ok(spy.calledOnce);
+          sinon.assert.calledWith(dispatch, {
+            type: ActionTypes.OPTION_SAVE_SUCCESS,
+            options: { optionName: "test", sectionName: "test", value: "test" }
+          });
+          sinon.assert.called(spyAddNotification);
         });
     });
 
-    it('calls optionSaveFailure when option save fails', () => {
-      const stub = sinon.stub(Backbone.Model.prototype, 'save');
-      const spy = sinon.spy(Actions, 'optionSaveFailure');
-      const promise = FauxtonAPI.Deferred();
-      promise.reject(failXhr);
-      stub.returns(promise);
+    it('dispatches OPTION_SAVE_FAILURE and shows notification when option add fails', () => {
+      const promise = FauxtonAPI.Promise.reject(new Error(''));
+      spySaveConfigOption.returns(promise);
+      const spyAddNotification = sinon.spy(FauxtonAPI, 'addNotification');
 
-      return Actions.saveOption(node, option)
+      return Actions.saveOption(node, option)(dispatch)
         .then(() => {
-          assert.ok(spy.calledOnce);
-        });
-    });
-
-    it('shows notification when option save fails', () => {
-      const stub = sinon.stub(Backbone.Model.prototype, 'save');
-      const spy = sinon.spy(FauxtonAPI, 'addNotification');
-      const promise = FauxtonAPI.Deferred();
-      promise.reject(failXhr);
-      stub.returns(promise);
-
-      return Actions.saveOption(node, option)
-        .then(() => {
-          assert.ok(spy.calledOnce);
+          sinon.assert.calledWith(dispatch, {
+            type: ActionTypes.OPTION_SAVE_FAILURE,
+            options: { optionName: "test", sectionName: "test", value: "test" }
+          });
+          sinon.assert.called(spyAddNotification);
         });
     });
   });
 
-  describe('delete', () => {
+  describe('deleteOption', () => {
     afterEach(() => {
-      restore(Actions.optionDeleteSuccess);
-      restore(Actions.optionDeleteFailure);
-      restore(FauxtonAPI.when);
+      spyDeleteConfigOption.reset();
+      dispatch.reset();
       restore(FauxtonAPI.addNotification);
-      restore(Backbone.Model.prototype.destroy);
-    });
-
-    it('calls optionDeleteSuccess when option delete succeeds', () => {
-      const stub = sinon.stub(Backbone.Model.prototype, 'destroy');
-      const spy = sinon.spy(Actions, 'optionDeleteSuccess');
-      const promise = FauxtonAPI.Deferred();
-      promise.resolve();
-      stub.returns(promise);
-
-      return Actions.deleteOption(node, option)
-        .then(() => {
-          assert.ok(spy.calledOnce);
-        });
-    });
-
-    it('shows notification when option delete succeeds', () => {
-      const stub = sinon.stub(Backbone.Model.prototype, 'destroy');
-      const spy = sinon.spy(FauxtonAPI, 'addNotification');
-      const promise = FauxtonAPI.Deferred();
-      promise.resolve();
-      stub.returns(promise);
-
-      return Actions.deleteOption(node, option)
-        .then(() => {
-          assert.ok(spy.calledOnce);
-        });
     });
 
-    it('calls optionDeleteFailure when option delete fails', () => {
-      const stub = sinon.stub(Backbone.Model.prototype, 'destroy');
-      const spy = sinon.spy(Actions, 'optionDeleteFailure');
-      const promise = FauxtonAPI.Deferred();
-      promise.reject(failXhr);
-      stub.returns(promise);
+    it('dispatches OPTION_DELETE_SUCCESS and shows notification when option add succeeds', () => {
+      const promise = FauxtonAPI.Promise.resolve();
+      spyDeleteConfigOption.returns(promise);
+      const spyAddNotification = sinon.spy(FauxtonAPI, 'addNotification');
 
-      return Actions.deleteOption(node, option)
+      return Actions.deleteOption(node, option)(dispatch)
         .then(() => {
-          assert.ok(spy.calledOnce);
+          sinon.assert.calledWith(dispatch, {
+            type: ActionTypes.OPTION_DELETE_SUCCESS,
+            options: { optionName: "test", sectionName: "test", value: "test" }
+          });
+          sinon.assert.called(spyAddNotification);
         });
     });
 
-    it('shows notification when option delete fails', () => {
-      const stub = sinon.stub(Backbone.Model.prototype, 'destroy');
-      const spy = sinon.spy(FauxtonAPI, 'addNotification');
-      const promise = FauxtonAPI.Deferred();
-      promise.reject(failXhr);
-      stub.returns(promise);
+    it('dispatches OPTION_DELETE_FAILURE and shows notification when option add fails', () => {
+      const promise = FauxtonAPI.Promise.reject(new Error(''));
+      spyDeleteConfigOption.returns(promise);
+      const spyAddNotification = sinon.spy(FauxtonAPI, 'addNotification');
 
-      return Actions.deleteOption(node, option)
+      return Actions.deleteOption(node, option)(dispatch)
         .then(() => {
-          assert.ok(spy.calledOnce);
+          sinon.assert.calledWith(dispatch, {
+            type: ActionTypes.OPTION_DELETE_FAILURE,
+            options: { optionName: "test", sectionName: "test", value: "test" }
+          });
+          sinon.assert.called(spyAddNotification);
         });
     });
   });
diff --git a/app/addons/config/__tests__/components.test.js b/app/addons/config/__tests__/components.test.js
index bd4c515e4..a250e3e35 100644
--- a/app/addons/config/__tests__/components.test.js
+++ b/app/addons/config/__tests__/components.test.js
@@ -10,85 +10,116 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import FauxtonAPI from "../../../core/api";
-import Views from "../components";
-import Actions from "../actions";
-import Stores from "../stores";
-import utils from "../../../../test/mocha/testUtils";
-import React from "react";
-import ReactDOM from "react-dom";
+import React from 'react';
 import {mount} from 'enzyme';
-import sinon from "sinon";
+import sinon from 'sinon';
+import FauxtonAPI from '../../../core/api';
+import AddOptionButton from '../components/AddOptionButton';
+import ConfigOption from '../components/ConfigOption';
+import ConfigOptionValue from '../components/ConfigOptionValue';
+import ConfigOptionTrash from '../components/ConfigOptionTrash';
+import ConfigTableScreen from '../components/ConfigTableScreen';
+import utils from '../../../../test/mocha/testUtils';
 
 FauxtonAPI.router = new FauxtonAPI.Router([]);
 const assert = utils.assert;
-const configStore = Stores.configStore;
 
 describe('Config Components', () => {
-  describe('ConfigTableController', () => {
-    let elm, node;
-
-    beforeEach(() => {
-      configStore._loading = false;
-      configStore._sections = {};
-      node = 'node2@127.0.0.1';
-      elm = mount(
-        <Views.ConfigTableController node={node}/>
-      );
-    });
+  describe('ConfigTableScreen', () => {
+    const options = [
+      {editing: false, header:true, sectionName: 'sec1', optionName: 'opt1', value: 'value1'},
+      {editing: false, header:false, sectionName: 'sec1', optionName: 'opt2', value: 'value2'}
+    ];
+    const node = 'test_node';
+    const defaultProps = {
+      saving: false,
+      loading: false,
+      deleteOption: () => {},
+      saveOption: () => {},
+      editOption: () => {},
+      cancelEdit: () => {},
+      fetchAndEditConfig: () => {},
+      node,
+      options
+    };
 
     it('deletes options', () => {
-      const spy = sinon.stub(Actions, 'deleteOption');
-      var option = {};
-
-      elm.instance().deleteOption(option);
-      assert.ok(spy.calledWith(node, option));
+      const spy = sinon.stub();
+      const wrapper = mount(<ConfigTableScreen
+        {...defaultProps}
+        deleteOption={spy}/>
+      );
+      wrapper.instance().deleteOption({});
+      sinon.assert.called(spy);
     });
 
     it('saves options', () => {
-      const spy = sinon.stub(Actions, 'saveOption');
-      var option = {};
-
-      elm.instance().saveOption(option);
-      assert.ok(spy.calledWith(node, option));
+      const spy = sinon.stub();
+      const wrapper = mount(<ConfigTableScreen
+        {...defaultProps}
+        saveOption={spy}/>
+      );
+      wrapper.instance().saveOption({});
+      sinon.assert.called(spy);
     });
 
     it('edits options', () => {
-      const spy = sinon.stub(Actions, 'editOption');
-      var option = {};
-
-      elm.instance().editOption(option);
-      assert.ok(spy.calledWith(option));
+      const spy = sinon.stub();
+      const wrapper = mount(<ConfigTableScreen
+        {...defaultProps}
+        editOption={spy}/>
+      );
+      wrapper.instance().editOption({});
+      sinon.assert.called(spy);
     });
 
     it('cancels editing', () => {
-      const spy = sinon.stub(Actions, 'cancelEdit');
-
-      elm.instance().cancelEdit();
-      assert.ok(spy.calledOnce);
+      const spy = sinon.stub();
+      const wrapper = mount(<ConfigTableScreen
+        {...defaultProps}
+        cancelEdit={spy}/>
+      );
+      wrapper.instance().cancelEdit();
+      sinon.assert.called(spy);
     });
   });
 
   describe('ConfigOption', () => {
-
+    const defaultProps = {
+      option: {},
+      saving: false,
+      onEdit: () => {},
+      onCancelEdit: () => {},
+      onSave: () => {},
+      onDelete: () => {}
+    };
     it('renders section name if the option is a header', () => {
       const option = {
         sectionName: 'test_section',
         optionName: 'test_option',
         value: 'test_value',
-        header: true
+        header: true,
+        editing: true
       };
 
-      const el = mount(<table><tbody><Views.ConfigOption option={option}/></tbody></table>);
+      const el = mount(<table><tbody><ConfigOption {...defaultProps} option={option}/></tbody></table>);
       assert.equal(el.find('th').text(), 'test_section');
     });
   });
 
   describe('ConfigOptionValue', () => {
+    const defaultProps = {
+      value: '',
+      editing: false,
+      onEdit: () => {},
+      onCancelEdit: () => {},
+      onSave: () => {}
+    };
+
     it('displays the value prop', () => {
       const el = mount(
         <table><tbody><tr>
-          <Views.ConfigOptionValue value={'test_value'}/>
+          <ConfigOptionValue {...defaultProps} value={'test_value'}/>
         </tr></tbody></table>
       );
 
@@ -99,18 +130,18 @@ describe('Config Components', () => {
       const spy = sinon.spy();
       const el = mount(
         <table><tbody><tr>
-          <Views.ConfigOptionValue value={'test_value'} onEdit={spy}/>
+          <ConfigOptionValue {...defaultProps} value={'test_value'} onEdit={spy}/>
         </tr></tbody></table>
       );
 
-      el.find(Views.ConfigOptionValue).simulate('click');
+      el.find(ConfigOptionValue).simulate('click');
       assert.ok(spy.calledOnce);
     });
 
     it('displays editing controls if editing', () => {
       const el = mount(
         <table><tbody><tr>
-          <Views.ConfigOptionValue value={'test_value'} editing/>
+          <ConfigOptionValue {...defaultProps} value={'test_value'} editing/>
         </tr></tbody></table>
       );
 
@@ -119,15 +150,13 @@ describe('Config Components', () => {
       assert.equal(el.find('button.btn-config-save').length, 1);
     });
 
-    it('disables input when save clicked', () => {
+    it('disables input when saving is set to true', () => {
       const el = mount(
         <table><tbody><tr>
-          <Views.ConfigOptionValue value={'test_value'} editing/>
+          <ConfigOptionValue {...defaultProps} value={'test_value'} editing={true} saving={true}/>
         </tr></tbody></table>
       );
 
-      el.find('input.config-value-input').simulate('change', {target: {value: 'value'}});
-      el.find('button.btn-config-save').simulate('click');
       assert.ok(el.find('input.config-value-input').prop('disabled'));
     });
 
@@ -136,7 +165,7 @@ describe('Config Components', () => {
       const spy = sinon.spy();
       const el = mount(
         <table><tbody><tr>
-          <Views.ConfigOptionValue value={'test_value'} editing onSave={spy}/>
+          <ConfigOptionValue {...defaultProps} value={'test_value'} editing onSave={spy}/>
         </tr></tbody></table>
       );
 
@@ -149,7 +178,7 @@ describe('Config Components', () => {
       const spy = sinon.spy();
       const el = mount(
         <table><tbody><tr>
-          <Views.ConfigOptionValue value={'test_value'} editing onCancelEdit={spy}/>
+          <ConfigOptionValue {...defaultProps} value={'test_value'} editing onCancelEdit={spy}/>
         </tr></tbody></table>
       );
 
@@ -162,7 +191,7 @@ describe('Config Components', () => {
 
     it.skip('displays delete modal when clicked', () => {
       const el = mount(
-        <Views.ConfigOptionTrash sectionName='test_section' optionName='test_option'/>
+        <ConfigOptionTrash sectionName='test_section' optionName='test_option'/>
       );
 
       el.simulate('click');
@@ -172,7 +201,7 @@ describe('Config Components', () => {
     it.skip('calls on delete when confirmation modal Okay button clicked', () => {
       const spy = sinon.spy();
       const el = mount(
-        <Views.ConfigOptionTrash onDelete={spy}/>
+        <ConfigOptionTrash onDelete={spy}/>
       );
 
       el.simulate('click');
@@ -181,19 +210,14 @@ describe('Config Components', () => {
     });
   });
 
-  describe('AddOptionController', () => {
-    let elm;
-
-    beforeEach(() => {
-      elm = mount(
-        <Views.AddOptionController node='node2@127.0.0.1'/>
-      );
-    });
-
+  //we need enzyme to support portals for this
+  describe.skip('AddOptionButton', () => {
     it('adds options', () => {
-      const spy = sinon.stub(Actions, 'addOption');
-
-      elm.instance().addOption();
+      const spy = sinon.stub();
+      const wrapper = mount(
+        <AddOptionButton onAdd={spy}/>
+      );
+      wrapper.instance().onAdd();
       assert.ok(spy.calledOnce);
     });
   });
@@ -202,7 +226,7 @@ describe('Config Components', () => {
   describe.skip('AddOptionButton', () => {
     it('displays add option controls when clicked', () => {
       const el = mount(
-        <Views.AddOptionButton/>
+        <AddOptionButton/>
       );
 
       el.find('button#add-option-button').simulate('click');
@@ -214,7 +238,7 @@ describe('Config Components', () => {
 
     it('does not hide popover if create clicked with invalid input', () => {
       const el = mount(
-        <Views.AddOptionButton/>
+        <AddOptionButton/>
       );
 
       el.find('button#add-option-button').simulate('click');
@@ -224,7 +248,7 @@ describe('Config Components', () => {
 
     it('does not add option if create clicked with invalid input', () => {
       const el = mount(
-        <Views.AddOptionButton/>
+        <AddOptionButton/>
       );
 
       el.find('button#add-option-button').simulate('click');
@@ -235,7 +259,7 @@ describe('Config Components', () => {
 
     it('does adds option if create clicked with valid input', () => {
       const el = mount(
-        <Views.AddOptionButton/>
+        <AddOptionButton/>
       );
 
       el.find('button#add-option-button').simulate('click');
@@ -246,7 +270,7 @@ describe('Config Components', () => {
     it('adds option when create clicked with valid input', () => {
       const spy = sinon.spy();
       const el = mount(
-        <Views.AddOptionButton onAdd={spy}/>
+        <AddOptionButton onAdd={spy}/>
       );
 
       el.find('button#add-option-button').simulate('click');
diff --git a/app/addons/config/__tests__/reducers.test.js b/app/addons/config/__tests__/reducers.test.js
new file mode 100644
index 000000000..346b699b3
--- /dev/null
+++ b/app/addons/config/__tests__/reducers.test.js
@@ -0,0 +1,114 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import utils from '../../../../test/mocha/testUtils';
+import ActionTypes from '../actiontypes';
+import reducer, {options} from '../reducers';
+
+const {assert} = utils;
+
+describe('Config Reducer', () => {
+  const editConfigAction = {
+    type: ActionTypes.EDIT_CONFIG,
+    options: {
+      sections: {
+        test: { b: 1, c: 2, a: 3 }
+      }
+    }
+  };
+  describe('fetchConfig', () => {
+    it('sorts options ascending', () => {
+      const newState = reducer(undefined, editConfigAction);
+      assert.ok(options(newState)[0].optionName, 'a');
+    });
+
+    it('sets the first option as the header', () => {
+      const newState = reducer(undefined, editConfigAction);
+      assert.isTrue(options(newState)[0].header);
+    });
+  });
+
+  describe('editOption', () => {
+    it('sets the option that is being edited', () => {
+      let newState = reducer(undefined, editConfigAction);
+      const opts = options(newState);
+      opts.forEach(el => {
+        assert.isFalse(el.editing);
+      });
+
+      const editOptionAction = {
+        type: ActionTypes.EDIT_OPTION,
+        options: {
+          sectionName: 'test',
+          optionName: 'b'
+        }
+      };
+      newState = reducer(newState, editOptionAction);
+      const opts2 = options(newState);
+      assert.isTrue(opts2[1].editing);
+    });
+  });
+
+  describe('saveOption', () => {
+    it('sets new option value', () => {
+      let newState = reducer(undefined, editConfigAction);
+      assert.equal(options(newState)[1].value, '1');
+
+      const saveOptionAction = {
+        type: ActionTypes.OPTION_SAVE_SUCCESS,
+        options: {
+          sectionName: 'test',
+          optionName: 'b',
+          value: 'new_value'
+        }
+      };
+      newState = reducer(newState, saveOptionAction);
+      assert.equal(options(newState)[1].value, 'new_value');
+    });
+  });
+
+  describe('deleteOption', () => {
+    it('deletes option from section', () => {
+      let newState = reducer(undefined, editConfigAction);
+      assert.equal(options(newState).length, 3);
+
+      const deleteOptionAction = {
+        type: ActionTypes.OPTION_DELETE_SUCCESS,
+        options: {
+          sectionName: 'test',
+          optionName: 'b'
+        }
+      };
+      newState = reducer(newState, deleteOptionAction);
+      assert.equal(options(newState).length, 2);
+    });
+
+    it('deletes section when all options are deleted', () => {
+      let newState = reducer(undefined, editConfigAction);
+      assert.equal(options(newState).length, 3);
+
+      const deleteOptionAction = {
+        type: ActionTypes.OPTION_DELETE_SUCCESS,
+        options: {
+          sectionName: 'test',
+          optionName: 'a'
+        }
+      };
+      newState = reducer(newState, deleteOptionAction);
+      deleteOptionAction.options.optionName = 'b';
+      newState = reducer(newState, deleteOptionAction);
+      deleteOptionAction.options.optionName = 'c';
+      newState = reducer(newState, deleteOptionAction);
+      assert.equal(options(newState).length, 0);
+    });
+  });
+});
diff --git a/app/addons/config/__tests__/stores.test.js b/app/addons/config/__tests__/stores.test.js
deleted file mode 100644
index 98ffb7e3d..000000000
--- a/app/addons/config/__tests__/stores.test.js
+++ /dev/null
@@ -1,94 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-import Stores from '../stores';
-import utils from '../../../../test/mocha/testUtils';
-
-const {assert} = utils;
-
-describe("ConfigStore", () => {
-  const configStore = Stores.configStore;
-
-  describe("mapSection", () => {
-    beforeEach(() => {
-      configStore._editOptionName = 'b';
-      configStore._editSectionName = 'test';
-    });
-
-    afterEach(() => {
-      configStore.reset();
-    });
-
-    it("sorts options ascending", () => {
-      const options = configStore.mapSection({ b: 1, c: 2, a: 3 }, 'test');
-      assert.equal(options[0].optionName, 'a');
-    });
-
-    it("sets the first option as the header", () => {
-      const options = configStore.mapSection({ b: 1, c: 2, a: 3 }, 'test');
-      assert.isTrue(options[0].header);
-    });
-
-    it("sets the option that is being edited", () => {
-      const options = configStore.mapSection({ b: 1, c: 2, a: 3 }, 'test');
-      assert.isTrue(options[1].editing);
-    });
-  });
-
-  describe("saveOption", () => {
-    let sectionName, optionName, value;
-
-    beforeEach(() => {
-      sectionName = 'a';
-      optionName = 'b';
-      value = 1;
-    });
-
-    afterEach(() => {
-      configStore.reset();
-    });
-
-    it("saves option to sections", () => {
-      configStore._sections = {};
-
-      configStore.saveOption(sectionName, optionName, value);
-      assert.deepEqual(configStore._sections, { a: { b: 1 } });
-    });
-  });
-
-  describe("deleteOption", () => {
-    let sectionName, optionName;
-
-    beforeEach(() => {
-      sectionName = 'a';
-      optionName = 'b';
-    });
-
-    afterEach(() => {
-      configStore.reset();
-    });
-
-    it("deletes option from section", () => {
-      configStore._sections = { a: { b: 1, c: 2 } };
-
-      configStore.deleteOption(sectionName, optionName);
-      assert.deepEqual(configStore._sections, { a: { c: 2 } });
-    });
-
-    it("deletes section when all options are deleted", () => {
-      configStore._sections = { a: { b: 1 } };
-
-      configStore.deleteOption(sectionName, optionName);
-      assert.deepEqual(configStore._sections, {});
-    });
-  });
-});
diff --git a/app/addons/config/actions.js b/app/addons/config/actions.js
index e633c78e0..ae4a97db4 100644
--- a/app/addons/config/actions.js
+++ b/app/addons/config/actions.js
@@ -12,114 +12,120 @@
 
 import ActionTypes from './actiontypes';
 import FauxtonAPI from '../../core/api';
-import Resources from './resources';
-
-export default {
-  fetchAndEditConfig (node) {
-    FauxtonAPI.dispatch({ type: ActionTypes.LOADING_CONFIG });
-
-    var configModel = new Resources.ConfigModel({ node });
-
-    configModel.fetch().then(() => this.editSections({ sections: configModel.get('sections'), node }));
-  },
-
-  editSections (options) {
-    FauxtonAPI.dispatch({ type: ActionTypes.EDIT_CONFIG, options });
-  },
-
-  editOption (options) {
-    FauxtonAPI.dispatch({ type: ActionTypes.EDIT_OPTION, options });
-  },
-
-  cancelEdit (options) {
-    FauxtonAPI.dispatch({ type: ActionTypes.CANCEL_EDIT, options });
-  },
-
-  saveOption (node, options) {
-    FauxtonAPI.dispatch({ type: ActionTypes.SAVING_OPTION, options });
+import * as ConfigAPI from './api';
+
+export const fetchAndEditConfig = (node) => (dispatch) => {
+  dispatch({ type: ActionTypes.LOADING_CONFIG });
+
+  ConfigAPI.fetchConfig(node).then(res => {
+    dispatch({
+      type: ActionTypes.EDIT_CONFIG,
+      options: {
+        sections: res.sections,
+        node
+      }
+    });
+  }).catch(err => {
+    FauxtonAPI.addNotification({
+      msg: 'Failed to load the configuration. ' + err.message,
+      type: 'error',
+      clear: true
+    });
+    dispatch({
+      type: ActionTypes.EDIT_CONFIG,
+      options: {
+        sections: [],
+        node
+      }
+    });
+  });
+};
 
-    var modelAttrs = options;
-    modelAttrs.node = node;
-    var optionModel = new Resources.OptionModel(modelAttrs);
+export const editOption = (options) => (dispatch) => {
+  dispatch({ type: ActionTypes.EDIT_OPTION, options });
+};
 
-    return optionModel.save()
-      .then(
-        () => this.optionSaveSuccess(options),
-        xhr => this.optionSaveFailure(options, JSON.parse(xhr.responseText).reason)
-      );
-  },
+export const cancelEdit = (options) => (dispatch) => {
+  dispatch({ type: ActionTypes.CANCEL_EDIT, options });
+};
 
-  optionSaveSuccess (options) {
-    FauxtonAPI.dispatch({ type: ActionTypes.OPTION_SAVE_SUCCESS, options });
-    FauxtonAPI.addNotification({
-      msg: `Option ${options.optionName} saved`,
-      type: 'success'
-    });
-  },
+export const saveOption = (node, options) => (dispatch) => {
+  dispatch({ type: ActionTypes.SAVING_OPTION, options });
 
-  optionSaveFailure (options, error) {
-    FauxtonAPI.dispatch({ type: ActionTypes.OPTION_SAVE_FAILURE, options });
-    FauxtonAPI.addNotification({
-      msg: `Option save failed: ${error}`,
-      type: 'error'
-    });
-  },
+  const { sectionName, optionName, value } = options;
+  return ConfigAPI.saveConfigOption(node, sectionName, optionName, value).then(
+    () => optionSaveSuccess(options, dispatch)
+  ).catch(
+    (err) => optionSaveFailure(options, err.message, dispatch)
+  );
+};
 
-  addOption (node, options) {
-    FauxtonAPI.dispatch({ type: ActionTypes.ADDING_OPTION });
+const optionSaveSuccess = (options, dispatch) => {
+  dispatch({ type: ActionTypes.OPTION_SAVE_SUCCESS, options });
+  FauxtonAPI.addNotification({
+    msg: `Option ${options.optionName} saved`,
+    type: 'success'
+  });
+};
 
-    var modelAttrs = options;
-    modelAttrs.node = node;
-    var optionModel = new Resources.OptionModel(modelAttrs);
+const optionSaveFailure = (options, error, dispatch) => {
+  dispatch({ type: ActionTypes.OPTION_SAVE_FAILURE, options });
+  FauxtonAPI.addNotification({
+    msg: `Option save failed: ${error}`,
+    type: 'error'
+  });
+};
 
-    return optionModel.save()
-      .then(
-        () => this.optionAddSuccess(options),
-        xhr => this.optionAddFailure(options, JSON.parse(xhr.responseText).reason)
-      );
-  },
+export const addOption = (node, options) => (dispatch) => {
+  dispatch({ type: ActionTypes.ADDING_OPTION });
 
-  optionAddSuccess (options) {
-    FauxtonAPI.dispatch({ type: ActionTypes.OPTION_ADD_SUCCESS, options });
-    FauxtonAPI.addNotification({
-      msg: `Option ${options.optionName} added`,
-      type: 'success'
-    });
-  },
+  const { sectionName, optionName, value } = options;
+  return ConfigAPI.saveConfigOption(node, sectionName, optionName, value).then(
+    () => optionAddSuccess(options, dispatch)
+  ).catch(
+    (err) => optionAddFailure(options, err.message, dispatch)
+  );
+};
 
-  optionAddFailure (options, error) {
-    FauxtonAPI.dispatch({ type: ActionTypes.OPTION_ADD_FAILURE, options });
-    FauxtonAPI.addNotification({
-      msg: `Option add failed: ${error}`,
-      type: 'error'
-    });
-  },
+const optionAddSuccess = (options, dispatch) => {
+  dispatch({ type: ActionTypes.OPTION_ADD_SUCCESS, options });
+  FauxtonAPI.addNotification({
+    msg: `Option ${options.optionName} added`,
+    type: 'success'
+  });
+};
 
-  deleteOption (node, options) {
-    FauxtonAPI.dispatch({ type: ActionTypes.DELETING_OPTION, options });
+const optionAddFailure = (options, error, dispatch) => {
+  dispatch({ type: ActionTypes.OPTION_ADD_FAILURE, options });
+  FauxtonAPI.addNotification({
+    msg: `Option add failed: ${error}`,
+    type: 'error'
+  });
+};
 
-    var modelAttrs = options;
-    modelAttrs.node = node;
-    var optionModel = new Resources.OptionModel(modelAttrs);
+export const deleteOption = (node, options) => (dispatch) => {
+  dispatch({ type: ActionTypes.DELETING_OPTION, options });
 
-    return optionModel.destroy()
-      .then(() => this.optionDeleteSuccess(options))
-      .catch((err) => this.optionDeleteFailure(options, err.message));
-  },
+  const { sectionName, optionName } = options;
+  return ConfigAPI.deleteConfigOption(node, sectionName, optionName).then(
+    () => optionDeleteSuccess(options, dispatch)
+  ).catch(
+    (err) => optionDeleteFailure(options, err.message, dispatch)
+  );
+};
 
-  optionDeleteSuccess (options) {
-    FauxtonAPI.dispatch({ type: ActionTypes.OPTION_DELETE_SUCCESS, options });
-    FauxtonAPI.addNotification({
-      msg: `Option ${options.optionName} deleted`,
-      type: 'success'
-    });
-  },
+const optionDeleteSuccess = (options, dispatch) => {
+  dispatch({ type: ActionTypes.OPTION_DELETE_SUCCESS, options });
+  FauxtonAPI.addNotification({
+    msg: `Option ${options.optionName} deleted`,
+    type: 'success'
+  });
+};
 
-  optionDeleteFailure (options, error) {
-    FauxtonAPI.dispatch({ type: ActionTypes.OPTION_DELETE_FAILURE, options });
-    FauxtonAPI.addNotification({
-      msg: `Option delete failed: ${error}`,
-      type: 'error'
-    });
-  }
+const optionDeleteFailure = (options, error, dispatch) => {
+  dispatch({ type: ActionTypes.OPTION_DELETE_FAILURE, options });
+  FauxtonAPI.addNotification({
+    msg: `Option delete failed: ${error}`,
+    type: 'error'
+  });
 };
diff --git a/app/addons/config/api.js b/app/addons/config/api.js
new file mode 100644
index 000000000..5a9f17d64
--- /dev/null
+++ b/app/addons/config/api.js
@@ -0,0 +1,54 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { get, put, deleteRequest } from '../../core/ajax';
+import Helpers from "../../helpers";
+
+export const configUrl = (node) => {
+  return Helpers.getServerUrl('/_node/' + node + '/_config');
+};
+
+export const fetchConfig = (node) => {
+  const url = configUrl(node);
+  return get(url).then((json) => {
+    if (json.error) {
+      throw new Error(json.reason);
+    }
+    return { sections: json };
+  });
+};
+
+export const optionUrl = (node, sectionName, optionName) => {
+  const endpointUrl = '/_node/' + node + '/_config/' +
+    encodeURIComponent(sectionName) + '/' + encodeURIComponent(optionName);
+  return Helpers.getServerUrl(endpointUrl);
+};
+
+export const saveConfigOption = (node, sectionName, optionName, value) => {
+  const url = optionUrl(node, sectionName, optionName);
+  return put(url, value).then((json) => {
+    if (json.error) {
+      throw new Error(json.reason || json.error);
+    }
+    return json;
+  });
+};
+
+export const deleteConfigOption = (node, sectionName, optionName) => {
+  const url = optionUrl(node, sectionName, optionName);
+  return deleteRequest(url).then((json) => {
+    if (json.error) {
+      throw new Error(json.reason);
+    }
+    return json;
+  });
+};
diff --git a/app/addons/config/base.js b/app/addons/config/base.js
index 2360ed72c..ffa6cab77 100644
--- a/app/addons/config/base.js
+++ b/app/addons/config/base.js
@@ -10,9 +10,11 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import FauxtonAPI from "../../core/api";
-import Config from "./routes";
-import "./assets/less/config.less";
+import FauxtonAPI from '../../core/api';
+import Config from './routes';
+import reducers from './reducers';
+import './assets/less/config.less';
+
 Config.initialize = function () {
   FauxtonAPI.addHeaderLink({
     title: 'Configuration',
@@ -22,4 +24,8 @@ Config.initialize = function () {
   });
 };
 
+FauxtonAPI.addReducers({
+  config: reducers
+});
+
 export default Config;
diff --git a/app/addons/config/components.js b/app/addons/config/components.js
deleted file mode 100644
index 31302f48f..000000000
--- a/app/addons/config/components.js
+++ /dev/null
@@ -1,433 +0,0 @@
-//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
-//  use this file except in compliance with the License. You may obtain a copy of
-//  the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-//  Unless required by applicable law or agreed to in writing, software
-//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-//  License for the specific language governing permissions and limitations under
-//  the License.
-
-import PropTypes from 'prop-types';
-
-import React from "react";
-import ReactDOM from "react-dom";
-import Stores from "./stores";
-import Actions from "./actions";
-import {Overlay, Button, Popover} from "react-bootstrap";
-import Components from "../components/react-components";
-import FauxtonComponents from "../fauxton/components";
-
-const configStore = Stores.configStore;
-
-class ConfigTableController extends React.Component {
-  getStoreState = () => {
-    return {
-      options: configStore.getOptions(),
-      loading: configStore.isLoading()
-    };
-  };
-
-  componentDidMount() {
-    configStore.on('change', this.onChange, this);
-  }
-
-  componentWillUnmount() {
-    configStore.off('change', this.onChange);
-  }
-
-  onChange = () => {
-    this.setState(this.getStoreState());
-  };
-
-  saveOption = (option) => {
-    Actions.saveOption(this.props.node, option);
-  };
-
-  deleteOption = (option) => {
-    Actions.deleteOption(this.props.node, option);
-  };
-
-  editOption = (option) => {
-    Actions.editOption(option);
-  };
-
-  cancelEdit = () => {
-    Actions.cancelEdit();
-  };
-
-  state = this.getStoreState();
-
-  render() {
-    if (this.state.loading) {
-      return (
-        <div className="view">
-          <Components.LoadLines />
-        </div>
-      );
-    }
-    return (
-      <ConfigTable
-        onDeleteOption={this.deleteOption}
-        onSaveOption={this.saveOption}
-        onEditOption={this.editOption}
-        onCancelEdit={this.cancelEdit}
-        options={this.state.options}/>
-    );
-  }
-}
-
-class ConfigTable extends React.Component {
-  createOptions = () => {
-    return _.map(this.props.options, (option) => (
-      <ConfigOption
-        option={option}
-        onDelete={this.props.onDeleteOption}
-        onSave={this.props.onSaveOption}
-        onEdit={this.props.onEditOption}
-        onCancelEdit={this.props.onCancelEdit}
-        key={`${option.sectionName}/${option.optionName}`}
-      />
-    ));
-  };
-
-  render() {
-    var options = this.createOptions();
-
-    return (
-      <table className="config table table-striped table-bordered">
-        <thead>
-          <tr>
-            <th id="config-section" width="22%">Section</th>
-            <th id="config-option" width="22%">Option</th>
-            <th id="config-value">Value</th>
-            <th id="config-trash"></th>
-          </tr>
-        </thead>
-        <tbody>
-          {options}
-        </tbody>
-      </table>
-    );
-  }
-}
-
-class ConfigOption extends React.Component {
-  onSave = (value) => {
-    var option = this.props.option;
-    option.value = value;
-    this.props.onSave(option);
-  };
-
-  onDelete = () => {
-    this.props.onDelete(this.props.option);
-  };
-
-  onEdit = () => {
-    this.props.onEdit(this.props.option);
-  };
-
-  render() {
-    return (
-      <tr className="config-item">
-        <th>{this.props.option.header && this.props.option.sectionName}</th>
-        <td>{this.props.option.optionName}</td>
-        <ConfigOptionValue
-          value={this.props.option.value}
-          editing={this.props.option.editing}
-          onSave={this.onSave}
-          onEdit={this.onEdit}
-          onCancelEdit={this.props.onCancelEdit}
-        />
-        <ConfigOptionTrash
-          optionName={this.props.option.optionName}
-          sectionName={this.props.option.sectionName}
-          onDelete={this.onDelete}/>
-      </tr>
-    );
-  }
-}
-
-class ConfigOptionValue extends React.Component {
-  static defaultProps = {
-    value: '',
-    editing: false,
-    saving: false,
-    onSave: () => null,
-    onEdit: () => null,
-    onCancelEdit: () => null
-  };
-
-  state = {
-    value: this.props.value,
-    editing: this.props.editing,
-    saving: this.props.saving
-  };
-
-  UNSAFE_componentWillReceiveProps(nextProps) {
-    if (this.props.value !== nextProps.value) {
-      this.setState({ saving: false });
-    }
-  }
-
-  onChange = (event) => {
-    this.setState({ value: event.target.value });
-  };
-
-  onSave = () => {
-    if (this.state.value !== this.props.value) {
-      this.setState({ saving: true });
-      this.props.onSave(this.state.value);
-    } else {
-      this.props.onCancelEdit();
-    }
-  };
-
-  getButtons = () => {
-    if (this.state.saving) {
-      return null;
-    }
-    return (
-      <span>
-        <button
-          className="btn btn-primary fonticon-ok-circled btn-small btn-config-save"
-          onClick={this.onSave.bind(this)}
-        />
-        <button
-          className="btn fonticon-cancel-circled btn-small btn-config-cancel"
-          onClick={this.props.onCancelEdit}
-        />
-      </span>
-    );
-
-  };
-
-  render() {
-    if (this.props.editing) {
-      return (
-        <td>
-          <div className="config-value-form">
-            <input
-              onChange={this.onChange.bind(this)}
-              defaultValue={this.props.value}
-              disabled={this.state.saving}
-              autoFocus type="text" className="config-value-input"
-            />
-            {this.getButtons()}
-          </div>
-        </td>
-      );
-    }
-    return (
-      <td className="config-show-value" onClick={this.props.onEdit}>
-        {this.props.value}
-      </td>
-    );
-
-  }
-}
-
-class ConfigOptionTrash extends React.Component {
-  constructor (props) {
-    super(props);
-    this.onDelete = this.onDelete.bind(this);
-    this.showModal = this.showModal.bind(this);
-    this.hideModal = this.hideModal.bind(this);
-    this.state = { show: false };
-  }
-
-  onDelete = () => {
-    this.props.onDelete();
-  };
-
-  showModal = () => {
-    this.setState({ show: true });
-  };
-
-  hideModal = () => {
-    this.setState({ show: false });
-  };
-
-  render() {
-    return (
-      <td className="text-center config-item-trash config-delete-value">
-        <i className="icon icon-trash" onClick={this.showModal}></i>
-        <FauxtonComponents.ConfirmationModal
-          text={`Are you sure you want to delete ${this.props.sectionName}/${this.props.optionName}?`}
-          onClose={this.hideModal}
-          onSubmit={this.onDelete}
-          visible={this.state.show}/>
-      </td>
-    );
-  }
-}
-
-class AddOptionController extends React.Component {
-  addOption = (option) => {
-    Actions.addOption(this.props.node, option);
-  };
-
-  render() {
-    return (
-      <AddOptionButton onAdd={this.addOption}/>
-    );
-  }
-}
-
-class AddOptionButton extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = this.getInitialState();
-  }
-
-  getInitialState () {
-    return {
-      sectionName: '',
-      optionName: '',
-      value: '',
-      show: false
-    };
-  }
-
-  isInputValid () {
-    if (this.state.sectionName !== ''
-      && this.state.optionName !== ''
-      && this.state.value !== '') {
-      return true;
-    }
-
-    return false;
-  }
-
-  updateSectionName (event) {
-    this.setState({ sectionName: event.target.value });
-  }
-
-  updateOptionName (event) {
-    this.setState({ optionName: event.target.value });
-  }
-
-  updateValue (event) {
-    this.setState({ value: event.target.value });
-  }
-
-  reset () {
-    this.setState(this.getInitialState());
-  }
-
-  onAdd () {
-    if (this.isInputValid()) {
-      var option = {
-        sectionName: this.state.sectionName,
-        optionName: this.state.optionName,
-        value: this.state.value
-      };
-
-      this.setState({ show: false });
-      this.props.onAdd(option);
-    }
-  }
-
-  togglePopover () {
-    this.setState({ show: !this.state.show });
-  }
-
-  hidePopover () {
-    this.setState({ show: false });
-  }
-
-  getPopover () {
-    return (
-      <Popover className="tray" id="add-option-popover" title="Add Option">
-        <input
-          className="input-section-name"
-          onChange={this.updateSectionName.bind(this)}
-          type="text" name="section" placeholder="Section" autoComplete="off" autoFocus/>
-        <input
-          className="input-option-name"
-          onChange={this.updateOptionName.bind(this)}
-          type="text" name="name" placeholder="Name"/>
-        <input
-          className="input-value"
-          onChange={this.updateValue.bind(this)}
-          type="text" name="value" placeholder="Value"/>
-        <a
-          className="btn btn-create"
-          onClick={this.onAdd.bind(this)}>
-          Create
-        </a>
-      </Popover>
-    );
-  }
-
-  render () {
-    return (
-      <div id="add-option-panel">
-        <Button
-          id="add-option-button"
-          onClick={this.togglePopover.bind(this)}
-          ref={node => this.target = node}>
-          <i className="icon icon-plus header-icon"></i>
-          Add Option
-        </Button>
-
-        <Overlay
-          show={this.state.show}
-          onHide={this.hidePopover.bind(this)}
-          placement="bottom"
-          rootClose={true}
-          target={() => this.target}>
-          {this.getPopover()}
-        </Overlay>
-      </div>
-    );
-  }
-}
-
-const TabItem = ({active, link, title}) => {
-  return (
-    <li className={active ? 'active' : ''}>
-      <a href={`#${link}`}>
-        {title}
-      </a>
-    </li>
-  );
-};
-
-TabItem.propTypes = {
-  active: PropTypes.bool.isRequired,
-  link: PropTypes.string.isRequired,
-  icon: PropTypes.string,
-  title: PropTypes.string.isRequired
-};
-
-const Tabs = ({sidebarItems, selectedTab}) => {
-  const tabItems = sidebarItems.map(item => {
-    return <TabItem
-      key={item.title}
-      active={selectedTab === item.title}
-      title={item.title}
-      link={item.link}
-    />;
-  });
-  return (
-    <nav className="sidenav">
-      <ul className="nav nav-list">
-        {tabItems}
-      </ul>
-    </nav>
-  );
-};
-
-export default {
-  Tabs,
-  ConfigTableController,
-  ConfigTable,
-  ConfigOption,
-  ConfigOptionValue,
-  ConfigOptionTrash,
-  AddOptionController,
-  AddOptionButton,
-};
diff --git a/app/addons/config/components/AddOptionButton.js b/app/addons/config/components/AddOptionButton.js
new file mode 100644
index 000000000..ddaf47943
--- /dev/null
+++ b/app/addons/config/components/AddOptionButton.js
@@ -0,0 +1,129 @@
+//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
+//  use this file except in compliance with the License. You may obtain a copy of
+//  the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+//  License for the specific language governing permissions and limitations under
+//  the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import {Button, Overlay, Popover} from 'react-bootstrap';
+
+export default class AddOptionButton extends React.Component {
+  static propTypes = {
+    onAdd: PropTypes.func.isRequired
+  };
+
+  constructor(props) {
+    super(props);
+    this.state = this.getInitialState();
+  }
+
+  getInitialState () {
+    return {
+      sectionName: '',
+      optionName: '',
+      value: '',
+      show: false
+    };
+  }
+
+  isInputValid () {
+    if (this.state.sectionName !== ''
+      && this.state.optionName !== ''
+      && this.state.value !== '') {
+      return true;
+    }
+
+    return false;
+  }
+
+  updateSectionName (event) {
+    this.setState({ sectionName: event.target.value });
+  }
+
+  updateOptionName (event) {
+    this.setState({ optionName: event.target.value });
+  }
+
+  updateValue (event) {
+    this.setState({ value: event.target.value });
+  }
+
+  reset () {
+    this.setState(this.getInitialState());
+  }
+
+  onAdd () {
+    if (this.isInputValid()) {
+      var option = {
+        sectionName: this.state.sectionName,
+        optionName: this.state.optionName,
+        value: this.state.value
+      };
+
+      this.setState({ show: false });
+      this.props.onAdd(option);
+    }
+  }
+
+  togglePopover () {
+    this.setState({ show: !this.state.show });
+  }
+
+  hidePopover () {
+    this.setState({ show: false });
+  }
+
+  getPopover () {
+    return (
+      <Popover className="tray" id="add-option-popover" title="Add Option">
+        <input
+          className="input-section-name"
+          onChange={this.updateSectionName.bind(this)}
+          type="text" name="section" placeholder="Section" autoComplete="off" autoFocus/>
+        <input
+          className="input-option-name"
+          onChange={this.updateOptionName.bind(this)}
+          type="text" name="name" placeholder="Name"/>
+        <input
+          className="input-value"
+          onChange={this.updateValue.bind(this)}
+          type="text" name="value" placeholder="Value"/>
+        <a
+          className="btn btn-create"
+          onClick={this.onAdd.bind(this)}>
+          Create
+        </a>
+      </Popover>
+    );
+  }
+
+  render () {
+    return (
+      <div id="add-option-panel">
+        <Button
+          id="add-option-button"
+          onClick={this.togglePopover.bind(this)}
+          ref={node => this.target = node}>
+          <i className="icon icon-plus header-icon"></i>
+          Add Option
+        </Button>
+
+        <Overlay
+          show={this.state.show}
+          onHide={this.hidePopover.bind(this)}
+          placement="bottom"
+          rootClose={true}
+          target={() => this.target}>
+          {this.getPopover()}
+        </Overlay>
+      </div>
+    );
+  }
+}
diff --git a/app/addons/config/components/AddOptionButtonContainer.js b/app/addons/config/components/AddOptionButtonContainer.js
new file mode 100644
index 000000000..331969a91
--- /dev/null
+++ b/app/addons/config/components/AddOptionButtonContainer.js
@@ -0,0 +1,35 @@
+//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
+//  use this file except in compliance with the License. You may obtain a copy of
+//  the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+//  License for the specific language governing permissions and limitations under
+//  the License.
+
+import { connect } from 'react-redux';
+import * as Actions from '../actions';
+import AddOptionButton from './AddOptionButton';
+
+
+const mapStateToProps = () => {
+  return {};
+};
+
+const mapDispatchToProps = (dispatch, ownProps) => {
+  return {
+    onAdd: (options) => {
+      dispatch(Actions.addOption(ownProps.node, options));
+    }
+  };
+};
+
+const AddOptionButtonContainer = connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(AddOptionButton);
+
+export default AddOptionButtonContainer;
diff --git a/app/addons/config/components/ConfigOption.js b/app/addons/config/components/ConfigOption.js
new file mode 100644
index 000000000..858eeb595
--- /dev/null
+++ b/app/addons/config/components/ConfigOption.js
@@ -0,0 +1,62 @@
+//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
+//  use this file except in compliance with the License. You may obtain a copy of
+//  the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+//  License for the specific language governing permissions and limitations under
+//  the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import ConfigOptionValue from './ConfigOptionValue';
+import ConfigOptionTrash from './ConfigOptionTrash';
+
+export default class ConfigOption extends React.Component {
+  static propTypes = {
+    option: PropTypes.object.isRequired,
+    saving: PropTypes.bool.isRequired,
+    onSave: PropTypes.func.isRequired,
+    onDelete: PropTypes.func.isRequired,
+    onEdit: PropTypes.func.isRequired,
+    onCancelEdit: PropTypes.func.isRequired
+  };
+
+  onSave = (value) => {
+    const option = this.props.option;
+    option.value = value;
+    this.props.onSave(option);
+  };
+
+  onDelete = () => {
+    this.props.onDelete(this.props.option);
+  };
+
+  onEdit = () => {
+    this.props.onEdit(this.props.option);
+  };
+
+  render() {
+    return (
+      <tr className="config-item">
+        <th>{this.props.option.header && this.props.option.sectionName}</th>
+        <td>{this.props.option.optionName}</td>
+        <ConfigOptionValue
+          value={this.props.option.value}
+          editing={this.props.option.editing}
+          saving={this.props.saving}
+          onSave={this.onSave}
+          onEdit={this.onEdit}
+          onCancelEdit={this.props.onCancelEdit}
+        />
+        <ConfigOptionTrash
+          optionName={this.props.option.optionName}
+          sectionName={this.props.option.sectionName}
+          onDelete={this.onDelete}/>
+      </tr>
+    );
+  }
+}
diff --git a/app/addons/config/components/ConfigOptionTrash.js b/app/addons/config/components/ConfigOptionTrash.js
new file mode 100644
index 000000000..a37f0315a
--- /dev/null
+++ b/app/addons/config/components/ConfigOptionTrash.js
@@ -0,0 +1,56 @@
+//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
+//  use this file except in compliance with the License. You may obtain a copy of
+//  the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+//  License for the specific language governing permissions and limitations under
+//  the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import FauxtonComponents from '../../fauxton/components';
+
+export default class ConfigOptionTrash extends React.Component {
+  constructor (props) {
+    super(props);
+    this.onDelete = this.onDelete.bind(this);
+    this.showModal = this.showModal.bind(this);
+    this.hideModal = this.hideModal.bind(this);
+    this.state = { show: false };
+  }
+
+  static propTypes = {
+    sectionName: PropTypes.string.isRequired,
+    optionName: PropTypes.string.isRequired,
+    onDelete: PropTypes.func.isRequired
+  };
+
+  onDelete = () => {
+    this.props.onDelete();
+  };
+
+  showModal = () => {
+    this.setState({ show: true });
+  };
+
+  hideModal = () => {
+    this.setState({ show: false });
+  };
+
+  render() {
+    return (
+      <td className="text-center config-item-trash config-delete-value">
+        <i className="icon icon-trash" onClick={this.showModal}></i>
+        <FauxtonComponents.ConfirmationModal
+          text={`Are you sure you want to delete ${this.props.sectionName}/${this.props.optionName}?`}
+          onClose={this.hideModal}
+          onSubmit={this.onDelete}
+          visible={this.state.show}/>
+      </td>
+    );
+  }
+}
diff --git a/app/addons/config/components/ConfigOptionValue.js b/app/addons/config/components/ConfigOptionValue.js
new file mode 100644
index 000000000..dc46aafda
--- /dev/null
+++ b/app/addons/config/components/ConfigOptionValue.js
@@ -0,0 +1,83 @@
+//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
+//  use this file except in compliance with the License. You may obtain a copy of
+//  the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+//  License for the specific language governing permissions and limitations under
+//  the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+
+export default class ConfigOptionValue extends React.Component {
+  static propTypes = {
+    value: PropTypes.string.isRequired,
+    editing: PropTypes.bool.isRequired,
+    onEdit: PropTypes.func.isRequired,
+    onCancelEdit: PropTypes.func.isRequired,
+    onSave: PropTypes.func.isRequired
+  };
+
+  state = {
+    value: this.props.value
+  };
+
+  onChange = (event) => {
+    this.setState({ value: event.target.value });
+  };
+
+  onSave = () => {
+    if (this.state.value !== this.props.value) {
+      this.props.onSave(this.state.value);
+    } else {
+      this.props.onCancelEdit();
+    }
+  };
+
+  getButtons = () => {
+    if (this.props.saving) {
+      return null;
+    }
+    return (
+      <span>
+        <button
+          className="btn btn-primary fonticon-ok-circled btn-small btn-config-save"
+          onClick={this.onSave.bind(this)}
+        />
+        <button
+          className="btn fonticon-cancel-circled btn-small btn-config-cancel"
+          onClick={this.props.onCancelEdit}
+        />
+      </span>
+    );
+
+  };
+
+  render() {
+    if (this.props.editing) {
+      return (
+        <td>
+          <div className="config-value-form">
+            <input
+              onChange={this.onChange.bind(this)}
+              defaultValue={this.props.value}
+              disabled={this.props.saving}
+              autoFocus type="text" className="config-value-input"
+            />
+            {this.getButtons()}
+          </div>
+        </td>
+      );
+    }
+    return (
+      <td className="config-show-value" onClick={this.props.onEdit}>
+        {this.props.value}
+      </td>
+    );
+
+  }
+}
diff --git a/app/addons/config/components/ConfigTable.js b/app/addons/config/components/ConfigTable.js
new file mode 100644
index 000000000..5e7a57d63
--- /dev/null
+++ b/app/addons/config/components/ConfigTable.js
@@ -0,0 +1,66 @@
+//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
+//  use this file except in compliance with the License. You may obtain a copy of
+//  the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+//  License for the specific language governing permissions and limitations under
+//  the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import ConfigOption from './ConfigOption';
+
+export default class ConfigTable extends React.Component {
+  static propTypes = {
+    options: PropTypes.arrayOf(PropTypes.shape({
+      editing: PropTypes.bool.isRequired,
+      header: PropTypes.bool,
+      optionName: PropTypes.string.isRequired,
+      sectionName: PropTypes.string.isRequired,
+      value: PropTypes.string
+    })).isRequired,
+    saving: PropTypes.bool.isRequired,
+    onDeleteOption: PropTypes.func.isRequired,
+    onEditOption: PropTypes.func.isRequired,
+    onSaveOption: PropTypes.func.isRequired,
+    onCancelEdit: PropTypes.func.isRequired
+  };
+
+  createOptions = () => {
+    return _.map(this.props.options, (option) => (
+      <ConfigOption
+        option={option}
+        saving={this.props.saving}
+        onDelete={this.props.onDeleteOption}
+        onSave={this.props.onSaveOption}
+        onEdit={this.props.onEditOption}
+        onCancelEdit={this.props.onCancelEdit}
+        key={`${option.sectionName}/${option.optionName}`}
+      />
+    ));
+  };
+
+  render() {
+    const options = this.createOptions();
+
+    return (
+      <table className="config table table-striped table-bordered">
+        <thead>
+          <tr>
+            <th id="config-section" width="22%">Section</th>
+            <th id="config-option" width="22%">Option</th>
+            <th id="config-value">Value</th>
+            <th id="config-trash"></th>
+          </tr>
+        </thead>
+        <tbody>
+          {options}
+        </tbody>
+      </table>
+    );
+  }
+}
diff --git a/app/addons/config/components/ConfigTableContainer.js b/app/addons/config/components/ConfigTableContainer.js
new file mode 100644
index 000000000..34750cf51
--- /dev/null
+++ b/app/addons/config/components/ConfigTableContainer.js
@@ -0,0 +1,58 @@
+//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
+//  use this file except in compliance with the License. You may obtain a copy of
+//  the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+//  License for the specific language governing permissions and limitations under
+//  the License.
+
+import { connect } from 'react-redux';
+import ConfigTableScreen from './ConfigTableScreen';
+import * as Actions from '../actions';
+import { options } from '../reducers';
+
+const mapStateToProps = ({ config }, ownProps) => {
+  return {
+    node: ownProps.node,
+    options: options(config),
+    loading: config.loading,
+    saving: config.saving,
+    editSectionName: config.editSectionName,
+    editOptionName: config.editOptionName,
+  };
+};
+
+const mapDispatchToProps = (dispatch) => {
+  return {
+    fetchAndEditConfig: (node) => {
+      dispatch(Actions.fetchAndEditConfig(node));
+    },
+
+    saveOption: (node, options) => {
+      dispatch(Actions.saveOption(node, options));
+    },
+
+    deleteOption: (node, options) => {
+      dispatch(Actions.deleteOption(node, options));
+    },
+
+    editOption: (options) => {
+      dispatch(Actions.editOption(options));
+    },
+
+    cancelEdit: (options) => {
+      dispatch(Actions.cancelEdit(options));
+    }
+  };
+};
+
+const ConfigTableContainer = connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(ConfigTableScreen);
+
+export default ConfigTableContainer;
diff --git a/app/addons/config/components/ConfigTableScreen.js b/app/addons/config/components/ConfigTableScreen.js
new file mode 100644
index 000000000..de4971ee3
--- /dev/null
+++ b/app/addons/config/components/ConfigTableScreen.js
@@ -0,0 +1,69 @@
+//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
+//  use this file except in compliance with the License. You may obtain a copy of
+//  the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+//  License for the specific language governing permissions and limitations under
+//  the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import Components from '../../components/react-components';
+import ConfigTable from './ConfigTable';
+
+export default class ConfigTableScreen extends React.Component {
+  static propTypes = {
+    options: PropTypes.array.isRequired,
+    loading: PropTypes.bool.isRequired,
+    saving: PropTypes.bool.isRequired,
+    saveOption: PropTypes.func.isRequired,
+    deleteOption: PropTypes.func.isRequired,
+    editOption: PropTypes.func.isRequired,
+    cancelEdit: PropTypes.func.isRequired,
+    fetchAndEditConfig: PropTypes.func.isRequired
+  };
+
+  constructor(props) {
+    super(props);
+    this.props.fetchAndEditConfig(this.props.node);
+  }
+
+  saveOption = (option) => {
+    this.props.saveOption(this.props.node, option);
+  };
+
+  deleteOption = (option) => {
+    this.props.deleteOption(this.props.node, option);
+  };
+
+  editOption = (option) => {
+    this.props.editOption(option);
+  };
+
+  cancelEdit = () => {
+    this.props.cancelEdit();
+  };
+
+  render() {
+    if (this.props.loading) {
+      return (
+        <div className="view">
+          <Components.LoadLines />
+        </div>
+      );
+    }
+    return (
+      <ConfigTable
+        saving={this.props.saving}
+        onDeleteOption={this.deleteOption}
+        onSaveOption={this.saveOption}
+        onEditOption={this.editOption}
+        onCancelEdit={this.cancelEdit}
+        options={this.props.options}/>
+    );
+  }
+}
diff --git a/app/addons/config/components/ConfigTabs.js b/app/addons/config/components/ConfigTabs.js
new file mode 100644
index 000000000..beab5eedb
--- /dev/null
+++ b/app/addons/config/components/ConfigTabs.js
@@ -0,0 +1,51 @@
+//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
+//  use this file except in compliance with the License. You may obtain a copy of
+//  the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+//  License for the specific language governing permissions and limitations under
+//  the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+
+const ConfigTabs = ({sidebarItems, selectedTab}) => {
+  const tabItems = sidebarItems.map(item => {
+    return <TabItem
+      key={item.title}
+      active={selectedTab === item.title}
+      title={item.title}
+      link={item.link}
+    />;
+  });
+  return (
+    <nav className="sidenav">
+      <ul className="nav nav-list">
+        {tabItems}
+      </ul>
+    </nav>
+  );
+};
+
+const TabItem = ({active, link, title}) => {
+  return (
+    <li className={active ? 'active' : ''}>
+      <a href={`#${link}`}>
+        {title}
+      </a>
+    </li>
+  );
+};
+
+TabItem.propTypes = {
+  active: PropTypes.bool.isRequired,
+  link: PropTypes.string.isRequired,
+  icon: PropTypes.string,
+  title: PropTypes.string.isRequired
+};
+
+export default ConfigTabs;
diff --git a/app/addons/config/layout.js b/app/addons/config/layout.js
index f3ff87b02..87471b45e 100644
--- a/app/addons/config/layout.js
+++ b/app/addons/config/layout.js
@@ -11,23 +11,25 @@
 // the License.
 
 import React from 'react';
-import ConfigComponents from "./components";
-import CORSComponents from "../cors/components";
-import {Breadcrumbs} from '../components/header-breadcrumbs';
-import {NotificationCenterButton} from '../fauxton/notifications/notifications';
-import {ApiBarWrapper} from '../components/layouts';
+import AddOptionButtonContainer from './components/AddOptionButtonContainer';
+import ConfigTableContainer from './components/ConfigTableContainer';
+import ConfigTabs from './components/ConfigTabs';
+import CORSComponents from '../cors/components';
+import { Breadcrumbs } from '../components/header-breadcrumbs';
+import { NotificationCenterButton } from '../fauxton/notifications/notifications';
+import { ApiBarWrapper } from '../components/layouts';
 
-export const ConfigHeader = ({node, crumbs, docURL, endpoint}) => {
+export const ConfigHeader = ({ node, crumbs, docURL, endpoint }) => {
   return (
     <header className="two-panel-header">
       <div className="flex-layout flex-row">
         <div id='breadcrumbs' className="faux__config-breadcrumbs">
-          <Breadcrumbs crumbs={crumbs}/>
+          <Breadcrumbs crumbs={crumbs} />
         </div>
         <div className="right-header-wrapper flex-layout flex-row flex-body">
           <div id="react-headerbar" className="flex-body"> </div>
           <div id="right-header" className="flex-fill">
-            <ConfigComponents.AddOptionController node={node} />
+            <AddOptionButtonContainer node={node} />
           </div>
           <ApiBarWrapper docURL={docURL} endpoint={endpoint} />
           <div id="notification-center-btn" className="flex-fill">
@@ -39,7 +41,7 @@ export const ConfigHeader = ({node, crumbs, docURL, endpoint}) => {
   );
 };
 
-export const ConfigLayout = ({showCors, docURL, node, endpoint, crumbs}) => {
+export const ConfigLayout = ({ showCors, docURL, node, endpoint, crumbs }) => {
   const sidebarItems = [
     {
       title: 'Main config',
@@ -51,7 +53,7 @@ export const ConfigLayout = ({showCors, docURL, node, endpoint, crumbs}) => {
     }
   ];
   const selectedTab = showCors ? 'CORS' : 'Main config';
-  const content = showCors ? <CORSComponents.CORSContainer node={node} url={endpoint}/> : <ConfigComponents.ConfigTableController node={node} />;
+  const content = showCors ? <CORSComponents.CORSContainer node={node} url={endpoint} /> : <ConfigTableContainer node={node} />;
   return (
     <div id="dashboard" className="with-sidebar">
       <ConfigHeader
@@ -62,7 +64,7 @@ export const ConfigLayout = ({showCors, docURL, node, endpoint, crumbs}) => {
       />
       <div className="with-sidebar tabs-with-sidebar content-area">
         <aside id="sidebar-content" className="scrollable">
-          <ConfigComponents.Tabs
+          <ConfigTabs
             sidebarItems={sidebarItems}
             selectedTab={selectedTab}
           />
diff --git a/app/addons/config/reducers.js b/app/addons/config/reducers.js
new file mode 100644
index 000000000..4952fb189
--- /dev/null
+++ b/app/addons/config/reducers.js
@@ -0,0 +1,172 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import ActionTypes from './actiontypes';
+
+const initialState = {
+  sections: {},
+  loading: true,
+  editSectionName: null,
+  editOptionName: null,
+  saving: false
+};
+
+function saveOption(state, { sectionName, optionName, value }) {
+  const newSections = {
+    ...state.sections
+  };
+
+  if (!newSections[sectionName]) {
+    newSections[sectionName] = {};
+  }
+
+  newSections[sectionName][optionName] = value || true;
+  return newSections;
+}
+
+function deleteOption(state, { sectionName, optionName }) {
+  const newSections = {
+    ...state.sections
+  };
+
+  if (newSections[sectionName]) {
+    // copy object
+    newSections[sectionName] = {...newSections[sectionName]};
+    delete newSections[sectionName][optionName];
+
+    if (Object.keys(newSections[sectionName]).length == 0) {
+      delete newSections[sectionName];
+    }
+  }
+  return newSections;
+}
+
+export function options(state) {
+  const sections = Object.keys(state.sections).map(sectionName => {
+    return {
+      sectionName,
+      options: mapSection(state, sectionName)
+    };
+  });
+  const sortedSections = sections.sort((a, b) => {
+    if (a.sectionName < b.sectionName) return -1;
+    else if (a.sectionName > b.sectionName) return 1;
+    return 0;
+  });
+  // flatten the list of options
+  return sortedSections.map(s => s.options).reduce((acc, options) => {
+    return acc.concat(options);
+  }, []);
+}
+
+function mapSection(state, sectionName) {
+  const section = state.sections[sectionName];
+  const options = Object.keys(section).map(optionName => {
+    return {
+      editing: isEditing(state, sectionName, optionName),
+      sectionName,
+      optionName,
+      value: section[optionName]
+    };
+  });
+  const sortedOptions = options.sort((a, b) => {
+    if (a.optionName < b.optionName) return -1;
+    else if (a.optionName > b.optionName) return 1;
+    return 0;
+  });
+  if (sortedOptions.length > 0) {
+    sortedOptions[0].header = true;
+  }
+  return sortedOptions;
+}
+
+function isEditing(state, sn, on) {
+  return sn === state.editSectionName && on === state.editOptionName;
+}
+
+export default function config(state = initialState, action) {
+  const { options } = action;
+
+  switch (action.type) {
+    case ActionTypes.EDIT_CONFIG:
+      return {
+        ...state,
+        sections: options.sections,
+        loading: false,
+        editOptionName: null,
+        editSectionName: null
+      };
+
+    case ActionTypes.EDIT_OPTION:
+      return {
+        ...state,
+        editSectionName: options.sectionName,
+        editOptionName: options.optionName
+      };
+
+    case ActionTypes.LOADING_CONFIG:
+      return {
+        ...state,
+        loading: true
+      };
+
+    case ActionTypes.CANCEL_EDIT:
+      return {
+        ...state,
+        editOptionName: null,
+        editSectionName: null
+      };
+
+    case ActionTypes.SAVING_OPTION:
+      return {
+        ...state,
+        saving: true
+      };
+
+    case ActionTypes.OPTION_SAVE_SUCCESS:
+      return {
+        ...state,
+        editOptionName: null,
+        editSectionName: null,
+        sections: saveOption(state, options),
+        saving: false
+      };
+
+    case ActionTypes.OPTION_SAVE_FAILURE:
+      return {
+        ...state,
+        saving: false
+      };
+
+    case ActionTypes.OPTION_ADD_SUCCESS:
+      return {
+        ...state,
+        sections: saveOption(state, options),
+        saving: false
+      };
+
+    case ActionTypes.OPTION_ADD_FAILURE:
+      return {
+        ...state,
+        saving: false
+      };
+
+    case ActionTypes.OPTION_DELETE_SUCCESS:
+      return {
+        ...state,
+        sections: deleteOption(state, options)
+      };
+
+    default:
+      return state;
+  }
+}
diff --git a/app/addons/config/resources.js b/app/addons/config/resources.js
deleted file mode 100644
index cd4401dec..000000000
--- a/app/addons/config/resources.js
+++ /dev/null
@@ -1,70 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-import FauxtonAPI from "../../core/api";
-import Helpers from "../../helpers";
-import { deleteRequest, put } from "../../core/ajax";
-
-var Config = FauxtonAPI.addon();
-
-Config.OptionModel = Backbone.Model.extend({
-  documentation: FauxtonAPI.constants.DOC_URLS.CONFIG,
-
-  url () {
-    if (!this.get('node')) {
-      throw new Error('no node set');
-    }
-    const endpointUrl = '/_node/' + this.get('node') + '/_config/' +
-      this.get('sectionName') + '/' + encodeURIComponent(this.get('optionName'));
-    return Helpers.getServerUrl(endpointUrl);
-  },
-
-  isNew () { return false; },
-
-  sync (method, model) {
-    let operation;
-    if (method === 'delete') {
-      operation = deleteRequest(
-        model.url()
-      );
-    } else {
-      operation = put(
-        model.url(),
-        model.get('value')
-      );
-    }
-
-    return operation.then((res) => {
-      if (res.error) {
-        throw new Error(res.reason || res.error);
-      }
-      return res;
-    });
-  }
-});
-
-Config.ConfigModel = Backbone.Model.extend({
-  documentation: FauxtonAPI.constants.DOC_URLS.CONFIG,
-
-  url () {
-    if (!this.get('node')) {
-      throw new Error('no node set');
-    }
-    return Helpers.getServerUrl('/_node/' + this.get('node') + '/_config');
-  },
-
-  parse (resp) {
-    return { sections: resp };
-  }
-});
-
-export default Config;
diff --git a/app/addons/config/routes.js b/app/addons/config/routes.js
index 4a21749cf..fbc622097 100644
--- a/app/addons/config/routes.js
+++ b/app/addons/config/routes.js
@@ -11,14 +11,12 @@
 // the License.
 
 import React from 'react';
-import FauxtonAPI from "../../core/api";
-import Config from "./resources";
-import ClusterActions from "../cluster/actions";
-import ConfigActions from "./actions";
+import FauxtonAPI from '../../core/api';
+import ClusterActions from '../cluster/actions';
+import * as ConfigAPI from './api';
 import Layout from './layout';
 
-
-var ConfigDisabledRouteObject = FauxtonAPI.RouteObject.extend({
+const ConfigDisabledRouteObject = FauxtonAPI.RouteObject.extend({
   selectedHeader: 'Configuration',
 
   routes: {
@@ -35,31 +33,23 @@ var ConfigDisabledRouteObject = FauxtonAPI.RouteObject.extend({
 });
 
 
-var ConfigPerNodeRouteObject = FauxtonAPI.RouteObject.extend({
+const ConfigPerNodeRouteObject = FauxtonAPI.RouteObject.extend({
   roles: ['_admin'],
   selectedHeader: 'Configuration',
 
-  apiUrl: function () {
-    return [this.configs.url(), this.configs.documentation];
-  },
-
   routes: {
     '_config/:node': 'configForNode',
     '_config/:node/cors': 'configCorsForNode'
   },
 
-  initialize: function (_a, options) {
-    var node = options[0];
-
-    this.configs = new Config.ConfigModel({ node: node });
+  initialize: function () {
   },
 
   configForNode: function (node) {
-    ConfigActions.fetchAndEditConfig(node);
     return <Layout
       node={node}
-      docURL={this.configs.documentation}
-      endpoint={this.configs.url()}
+      docURL={FauxtonAPI.constants.DOC_URLS.CONFIG}
+      endpoint={ConfigAPI.configUrl(node)}
       crumbs={[{ name: 'Config' }]}
       showCors={false}
     />;
@@ -68,14 +58,15 @@ var ConfigPerNodeRouteObject = FauxtonAPI.RouteObject.extend({
   configCorsForNode: function (node) {
     return <Layout
       node={node}
-      docURL={this.configs.documentation}
-      endpoint={this.configs.url()}
+      docURL={FauxtonAPI.constants.DOC_URLS.CONFIG}
+      endpoint={ConfigAPI.configUrl(node)}
       crumbs={[{ name: 'Config' }]}
       showCors={true}
     />;
   }
 });
 
+const Config = FauxtonAPI.addon();
 Config.RouteObjects = [ConfigPerNodeRouteObject, ConfigDisabledRouteObject];
 
 export default Config;
diff --git a/app/addons/config/stores.js b/app/addons/config/stores.js
deleted file mode 100644
index c97f206de..000000000
--- a/app/addons/config/stores.js
+++ /dev/null
@@ -1,149 +0,0 @@
-//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
-//  use this file except in compliance with the License. You may obtain a copy of
-//  the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-//  Unless required by applicable law or agreed to in writing, software
-//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-//  License for the specific language governing permissions and limitations under
-//  the License.
-
-import FauxtonAPI from '../../core/api';
-import ActionTypes from './actiontypes';
-
-var ConfigStore = FauxtonAPI.Store.extend({
-  initialize () {
-    this.reset();
-  },
-
-  reset () {
-    this._sections = {};
-    this._loading = true;
-    this._editSectionName = null;
-    this._editOptionName = null;
-  },
-
-  editConfig (sections) {
-    this._sections = sections;
-    this._loading = false;
-    this._editSectionName = null;
-    this._editOptionName = null;
-  },
-
-  getOptions () {
-    var sections = _.sortBy(
-      _.map(this._sections, (section, sectionName) => {
-        return {
-          sectionName,
-          options: this.mapSection(section, sectionName)
-        };
-      }),
-      s => s.sectionName
-    );
-
-    return _.flatten(_.map(sections, s => s.options));
-  },
-
-  mapSection (section, sectionName) {
-    var options = _.sortBy(
-      _.map(section, (value, optionName) => ({
-        editing: this.isEditing(sectionName, optionName),
-        sectionName, optionName, value
-      })), o => o.optionName
-    );
-
-    options[0].header = true;
-
-    return options;
-  },
-
-  editOption (sn, on) {
-    this._editSectionName = sn;
-    this._editOptionName = on;
-  },
-
-  isEditing (sn, on) {
-    return sn == this._editSectionName && on == this._editOptionName;
-  },
-
-  stopEditing () {
-    this._editOptionName = null;
-    this._editSectionName = null;
-  },
-
-  setLoading () {
-    this._loading = true;
-  },
-
-  isLoading () {
-    return this._loading;
-  },
-
-  saveOption (sectionName, optionName, value) {
-    if (!this._sections[sectionName]) {
-      this._sections[sectionName] = {};
-    }
-
-    this._sections[sectionName][optionName] = value || true;
-  },
-
-  deleteOption (sectionName, optionName) {
-    if (this._sections[sectionName]) {
-      delete this._sections[sectionName][optionName];
-
-      if (Object.keys(this._sections[sectionName]).length == 0) {
-        delete this._sections[sectionName];
-      }
-    }
-  },
-
-  dispatch (action) {
-    if (action.options) {
-      var sectionName = action.options.sectionName;
-      var optionName = action.options.optionName;
-      var value = action.options.value;
-    }
-
-    switch (action.type) {
-      case ActionTypes.EDIT_CONFIG:
-        this.editConfig(action.options.sections, action.options.node);
-        break;
-
-      case ActionTypes.LOADING_CONFIG:
-        this.setLoading();
-        break;
-
-      case ActionTypes.EDIT_OPTION:
-        this.editOption(sectionName, optionName);
-        break;
-
-      case ActionTypes.CANCEL_EDIT:
-        this.stopEditing();
-        break;
-
-      case ActionTypes.OPTION_SAVE_SUCCESS:
-        this.saveOption(sectionName, optionName, value);
-        this.stopEditing();
-        break;
-
-      case ActionTypes.OPTION_ADD_SUCCESS:
-        this.saveOption(sectionName, optionName, value);
-        break;
-
-      case ActionTypes.OPTION_DELETE_SUCCESS:
-        this.deleteOption(sectionName, optionName);
-        break;
-    }
-
-    this.triggerChange();
-  }
-});
-
-var configStore = new ConfigStore();
-configStore.dispatchToken = FauxtonAPI.dispatcher.register(configStore.dispatch.bind(configStore));
-
-export default {
-  configStore: configStore
-};


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services