Update config addon to use redux (#1138)

* Split components into separate files

* Use config addon to use redux

* Update tests
diff --git a/app/addons/config/__tests__/actions.test.js b/app/addons/config/__tests__/actions.test.js
index 410dabc..d9e9649 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 @@
     optionName: 'test',
     value: 'test'
   };
-  const failXhr = { responseText: '{}' };
+  const spySaveConfigOption = sinon.stub(ConfigAPI, 'saveConfigOption');
+  const spyDeleteConfigOption = sinon.stub(ConfigAPI, 'deleteConfigOption');
+  const dispatch = sinon.stub();
 
-  describe('add', () => {
+  describe('addOption', () => {
+
     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);
+    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 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_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);
-        });
-    });
-
-    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);
-
-      return Actions.addOption(node, option)
-        .then(() => {
-          assert.ok(spy.calledOnce);
-        });
-    });
-
-    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);
-
-      return Actions.addOption(node, option)
-        .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);
+    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('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_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('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);
-
-      return Actions.saveOption(node, option)
-        .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);
+    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 succeeds', () => {
-      const stub = sinon.stub(Backbone.Model.prototype, 'destroy');
-      const spy = sinon.spy(FauxtonAPI, 'addNotification');
-      const promise = FauxtonAPI.Deferred();
-      promise.resolve();
-      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);
-        });
-    });
-
-    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);
-
-      return Actions.deleteOption(node, option)
-        .then(() => {
-          assert.ok(spy.calledOnce);
-        });
-    });
-
-    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);
-
-      return Actions.deleteOption(node, option)
-        .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 bd4c515..a250e3e 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 @@
       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 @@
       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 @@
       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 @@
       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 @@
 
     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 @@
     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('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.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 @@
 
     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 @@
 
     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 @@
 
     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 @@
     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 0000000..346b699
--- /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 98ffb7e..0000000
--- 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 e633c78..ae4a97d 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';
+import * as ConfigAPI from './api';
 
-export default {
-  fetchAndEditConfig (node) {
-    FauxtonAPI.dispatch({ type: ActionTypes.LOADING_CONFIG });
+export const fetchAndEditConfig = (node) => (dispatch) => {
+  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 });
-
-    var modelAttrs = options;
-    modelAttrs.node = node;
-    var optionModel = new Resources.OptionModel(modelAttrs);
-
-    return optionModel.save()
-      .then(
-        () => this.optionSaveSuccess(options),
-        xhr => this.optionSaveFailure(options, JSON.parse(xhr.responseText).reason)
-      );
-  },
-
-  optionSaveSuccess (options) {
-    FauxtonAPI.dispatch({ type: ActionTypes.OPTION_SAVE_SUCCESS, options });
-    FauxtonAPI.addNotification({
-      msg: `Option ${options.optionName} saved`,
-      type: 'success'
+  ConfigAPI.fetchConfig(node).then(res => {
+    dispatch({
+      type: ActionTypes.EDIT_CONFIG,
+      options: {
+        sections: res.sections,
+        node
+      }
     });
-  },
-
-  optionSaveFailure (options, error) {
-    FauxtonAPI.dispatch({ type: ActionTypes.OPTION_SAVE_FAILURE, options });
+  }).catch(err => {
     FauxtonAPI.addNotification({
-      msg: `Option save failed: ${error}`,
-      type: 'error'
+      msg: 'Failed to load the configuration. ' + err.message,
+      type: 'error',
+      clear: true
     });
-  },
-
-  addOption (node, options) {
-    FauxtonAPI.dispatch({ type: ActionTypes.ADDING_OPTION });
-
-    var modelAttrs = options;
-    modelAttrs.node = node;
-    var optionModel = new Resources.OptionModel(modelAttrs);
-
-    return optionModel.save()
-      .then(
-        () => this.optionAddSuccess(options),
-        xhr => this.optionAddFailure(options, JSON.parse(xhr.responseText).reason)
-      );
-  },
-
-  optionAddSuccess (options) {
-    FauxtonAPI.dispatch({ type: ActionTypes.OPTION_ADD_SUCCESS, options });
-    FauxtonAPI.addNotification({
-      msg: `Option ${options.optionName} added`,
-      type: 'success'
+    dispatch({
+      type: ActionTypes.EDIT_CONFIG,
+      options: {
+        sections: [],
+        node
+      }
     });
-  },
+  });
+};
 
-  optionAddFailure (options, error) {
-    FauxtonAPI.dispatch({ type: ActionTypes.OPTION_ADD_FAILURE, options });
-    FauxtonAPI.addNotification({
-      msg: `Option add failed: ${error}`,
-      type: 'error'
-    });
-  },
+export const editOption = (options) => (dispatch) => {
+  dispatch({ type: ActionTypes.EDIT_OPTION, options });
+};
 
-  deleteOption (node, options) {
-    FauxtonAPI.dispatch({ type: ActionTypes.DELETING_OPTION, options });
+export const cancelEdit = (options) => (dispatch) => {
+  dispatch({ type: ActionTypes.CANCEL_EDIT, options });
+};
 
-    var modelAttrs = options;
-    modelAttrs.node = node;
-    var optionModel = new Resources.OptionModel(modelAttrs);
+export const saveOption = (node, options) => (dispatch) => {
+  dispatch({ type: ActionTypes.SAVING_OPTION, options });
 
-    return optionModel.destroy()
-      .then(() => this.optionDeleteSuccess(options))
-      .catch((err) => this.optionDeleteFailure(options, err.message));
-  },
+  const { sectionName, optionName, value } = options;
+  return ConfigAPI.saveConfigOption(node, sectionName, optionName, value).then(
+    () => optionSaveSuccess(options, dispatch)
+  ).catch(
+    (err) => optionSaveFailure(options, err.message, dispatch)
+  );
+};
 
-  optionDeleteSuccess (options) {
-    FauxtonAPI.dispatch({ type: ActionTypes.OPTION_DELETE_SUCCESS, options });
-    FauxtonAPI.addNotification({
-      msg: `Option ${options.optionName} deleted`,
-      type: 'success'
-    });
-  },
+const optionSaveSuccess = (options, dispatch) => {
+  dispatch({ type: ActionTypes.OPTION_SAVE_SUCCESS, options });
+  FauxtonAPI.addNotification({
+    msg: `Option ${options.optionName} saved`,
+    type: 'success'
+  });
+};
 
-  optionDeleteFailure (options, error) {
-    FauxtonAPI.dispatch({ type: ActionTypes.OPTION_DELETE_FAILURE, options });
-    FauxtonAPI.addNotification({
-      msg: `Option delete failed: ${error}`,
-      type: 'error'
-    });
-  }
+const optionSaveFailure = (options, error, dispatch) => {
+  dispatch({ type: ActionTypes.OPTION_SAVE_FAILURE, options });
+  FauxtonAPI.addNotification({
+    msg: `Option save failed: ${error}`,
+    type: 'error'
+  });
+};
+
+export const addOption = (node, options) => (dispatch) => {
+  dispatch({ type: ActionTypes.ADDING_OPTION });
+
+  const { sectionName, optionName, value } = options;
+  return ConfigAPI.saveConfigOption(node, sectionName, optionName, value).then(
+    () => optionAddSuccess(options, dispatch)
+  ).catch(
+    (err) => optionAddFailure(options, err.message, dispatch)
+  );
+};
+
+const optionAddSuccess = (options, dispatch) => {
+  dispatch({ type: ActionTypes.OPTION_ADD_SUCCESS, options });
+  FauxtonAPI.addNotification({
+    msg: `Option ${options.optionName} added`,
+    type: 'success'
+  });
+};
+
+const optionAddFailure = (options, error, dispatch) => {
+  dispatch({ type: ActionTypes.OPTION_ADD_FAILURE, options });
+  FauxtonAPI.addNotification({
+    msg: `Option add failed: ${error}`,
+    type: 'error'
+  });
+};
+
+export const deleteOption = (node, options) => (dispatch) => {
+  dispatch({ type: ActionTypes.DELETING_OPTION, options });
+
+  const { sectionName, optionName } = options;
+  return ConfigAPI.deleteConfigOption(node, sectionName, optionName).then(
+    () => optionDeleteSuccess(options, dispatch)
+  ).catch(
+    (err) => optionDeleteFailure(options, err.message, dispatch)
+  );
+};
+
+const optionDeleteSuccess = (options, dispatch) => {
+  dispatch({ type: ActionTypes.OPTION_DELETE_SUCCESS, options });
+  FauxtonAPI.addNotification({
+    msg: `Option ${options.optionName} deleted`,
+    type: 'success'
+  });
+};
+
+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 0000000..5a9f17d
--- /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 2360ed7..ffa6cab 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 @@
   });
 };
 
+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 31302f4..0000000
--- 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 0000000..ddaf479
--- /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 0000000..331969a
--- /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 0000000..858eeb5
--- /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 0000000..a37f031
--- /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 0000000..dc46aaf
--- /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 0000000..5e7a57d
--- /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 0000000..34750cf
--- /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 0000000..de4971e
--- /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 0000000..beab5ee
--- /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 f3ff87b..87471b4 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 ConfigLayout = ({showCors, docURL, node, endpoint, crumbs}) => {
+export const ConfigLayout = ({ showCors, docURL, node, endpoint, crumbs }) => {
   const sidebarItems = [
     {
       title: 'Main config',
@@ -51,7 +53,7 @@
     }
   ];
   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 @@
       />
       <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 0000000..4952fb1
--- /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 cd4401d..0000000
--- 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 4a21749..fbc6220 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 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 @@
   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 c97f206..0000000
--- 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
-};