You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ro...@apache.org on 2016/09/13 12:31:09 UTC

[1/2] fauxton commit: updated refs/heads/master to 4f633e5

Repository: couchdb-fauxton
Updated Branches:
  refs/heads/master e51775b8a -> 4f633e508


react: port config section to react

PR: #750
PR-URL: https://github.com/apache/couchdb-fauxton/pull/750
Reviewed-By: Robert Kowalski <ro...@kowalski.gd>


Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/ee1a92b0
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/ee1a92b0
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/ee1a92b0

Branch: refs/heads/master
Commit: ee1a92b02e68d92dd86e5979d5d8f2d100fb24e7
Parents: e51775b
Author: samk <sa...@gmail.com>
Authored: Sun Jul 24 21:55:11 2016 +1000
Committer: Robert Kowalski <ro...@apache.org>
Committed: Tue Sep 13 14:22:41 2016 +0200

----------------------------------------------------------------------
 app/addons/config/actions.js                    | 121 ++++++
 app/addons/config/actiontypes.js                |  27 ++
 app/addons/config/assets/less/config.less       | 107 ++++-
 app/addons/config/components.react.jsx          | 398 +++++++++++++++++++
 app/addons/config/resources.js                  |  67 +---
 app/addons/config/routes.js                     |  16 +-
 app/addons/config/stores.js                     | 149 +++++++
 .../config/templates/add_config_option.html     |  26 --
 app/addons/config/templates/dashboard.html      |  24 --
 app/addons/config/templates/header.html         |  15 -
 app/addons/config/templates/item.html           |  40 --
 app/addons/config/tests/actionsSpec.js          | 193 +++++++++
 .../config/tests/componentsSpec.react.jsx       | 306 ++++++++++++++
 app/addons/config/tests/configSpec.js           | 162 --------
 app/addons/config/tests/storesSpec.js           |  94 +++++
 app/addons/config/views.js                      | 229 -----------
 devserver.js                                    |   4 +-
 tasks/helper.js                                 |   2 +-
 webpack.config.dev.js                           |   4 +-
 19 files changed, 1411 insertions(+), 573 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/actions.js
----------------------------------------------------------------------
diff --git a/app/addons/config/actions.js b/app/addons/config/actions.js
new file mode 100644
index 0000000..00cfc23
--- /dev/null
+++ b/app/addons/config/actions.js
@@ -0,0 +1,121 @@
+//  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';
+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 });
+
+    var modelAttrs = options;
+    modelAttrs.node = node;
+    var optionModel = new Resources.OptionModel(modelAttrs);
+
+    optionModel.save()
+      .then(() => this.optionSaveSuccess(options))
+      .fail(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'
+    });
+  },
+
+  optionSaveFailure (options, error) {
+    FauxtonAPI.dispatch({ type: ActionTypes.OPTION_SAVE_FAILURE, options });
+    FauxtonAPI.addNotification({
+      msg: `Option save failed: ${error}`,
+      type: 'error'
+    });
+  },
+
+  addOption (node, options) {
+    FauxtonAPI.dispatch({ type: ActionTypes.ADDING_OPTION });
+
+    var modelAttrs = options;
+    modelAttrs.node = node;
+    var optionModel = new Resources.OptionModel(modelAttrs);
+
+    optionModel.save()
+      .then(() => this.optionAddSuccess(options))
+      .fail(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'
+    });
+  },
+
+  optionAddFailure (options, error) {
+    FauxtonAPI.dispatch({ type: ActionTypes.OPTION_ADD_FAILURE, options });
+    FauxtonAPI.addNotification({
+      msg: `Option add failed: ${error}`,
+      type: 'error'
+    });
+  },
+
+  deleteOption (node, options) {
+    FauxtonAPI.dispatch({ type: ActionTypes.DELETING_OPTION, options });
+
+    var modelAttrs = options;
+    modelAttrs.node = node;
+    var optionModel = new Resources.OptionModel(modelAttrs);
+
+    optionModel.destroy()
+      .then(() => this.optionDeleteSuccess(options))
+      .fail((xhr) => this.optionDeleteFailure(options, JSON.parse(xhr.responseText).reason));
+  },
+
+  optionDeleteSuccess (options) {
+    FauxtonAPI.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'
+    });
+  }
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/actiontypes.js
----------------------------------------------------------------------
diff --git a/app/addons/config/actiontypes.js b/app/addons/config/actiontypes.js
new file mode 100644
index 0000000..9391ac3
--- /dev/null
+++ b/app/addons/config/actiontypes.js
@@ -0,0 +1,27 @@
+//  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.
+
+export default {
+  EDIT_CONFIG: 'EDIT_CONFIG',
+  LOADING_CONFIG: 'LOADING_CONFIG',
+  EDIT_OPTION: 'EDIT_OPTION',
+  CANCEL_EDIT: 'CANCEL_EDIT',
+  SAVING_OPTION: 'SAVING_OPTION',
+  OPTION_SAVE_SUCCESS: 'OPTION_SAVE_SUCCESS',
+  OPTION_SAVE_FAILURE: 'OPTION_SAVE_FAILURE',
+  DELETING_OPTION: 'DELETING_OPTION',
+  OPTION_DELETE_SUCCESS: 'OPTION_DELETE_SUCCESS',
+  OPTION_DELETE_FAILURE: 'OPTION_DELETE_FAILURE',
+  ADDING_OPTION: 'ADDING_OPTION',
+  OPTION_ADD_SUCCESS: 'OPTION_ADD_SUCCESS',
+  OPTION_ADD_FAILURE: 'OPTION_ADD_FAILURE'
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/assets/less/config.less
----------------------------------------------------------------------
diff --git a/app/addons/config/assets/less/config.less b/app/addons/config/assets/less/config.less
index cd57171..f74b2ec 100644
--- a/app/addons/config/assets/less/config.less
+++ b/app/addons/config/assets/less/config.less
@@ -9,31 +9,55 @@
 //  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 "../../../../../assets/less/variables.less";
 @import "../../../../../assets/less/bootstrap/mixins.less";
 
-
-
 .config-item {
   height: 65px;
 
-  .js-value-input {
+  .config-value-form {
+    button {
+      margin-left: 8px;
+    }
+  }
+
+  .config-value-input {
     width: 80%;
     margin: 0;
   }
-  .js-show-value,
-  .js-delete-value {
+
+  .config-show-value,
+  .config-delete-value {
     cursor: pointer;
   }
-  .js-hidden {
-    display: none;
-  }
+
   .text-center {
     text-align: center;
   }
 
   button { width: 7%;}
+
+  transition: background-color 100ms;
+}
+
+.config-item:hover {
+  background-color: #e0e0e0;
+}
+
+.table-striped tbody > tr.config-item:nth-child(odd) > td,
+.table-striped tbody > tr.config-item:nth-child(odd) > th {
+  transition: background-color 100ms;
+}
+
+.table-striped tbody > tr.config-item:nth-child(odd):hover > td,
+.table-striped tbody > tr.config-item:nth-child(odd):hover > th {
+  background-color: #e7e7e7;
+}
+
+.config-item-deleting {
+  text-decoration: line-through;
+  text-decoration-color: red;
 }
 
 #config-trash {
@@ -55,9 +79,14 @@ table.config {
   }
 }
 
-#add-new-section {
+#add-option-button {
   line-height: 40px;
   cursor: pointer;
+  border-left: 1px solid #ccc;
+  border-right: none;
+  border-top: none;
+  border-bottom: none;
+  padding: 12px 14px;
 
   &:hover {
     transition: none;
@@ -67,8 +96,7 @@ table.config {
 }
 
 #add-section-button {
-  border-left: 1px solid #ccc;
-  padding: 12px 14px;
+
 }
 
 .add-section-tray {
@@ -112,6 +140,61 @@ table.config {
   }
 }
 
+#add-option-popover {
+  background-color: #333;
+  color: white;
+  padding: 20px;
+  border-radius: 0;
+  width: 300px;
+  max-width: none;
+  display: block;
+
+  .popover-content {
+    padding: 0;
+  }
+
+  .popover-title {
+    font-size: 16px;
+    padding: 0;
+    margin: 10px 0;
+    border: none;
+    display: block;
+    text-align: left;
+    color: #fff;
+    text-shadow: none;
+    height: auto;
+    line-height: 1em;
+    background-color: #333;
+  }
+
+  input {
+    width: 100%;
+  }
+
+  a.btn {
+    color: white;
+    background-color: @linkColor;
+    line-height: 1.5em;
+    border: 0;
+    padding: 10px 10px 9px;
+    font-size: 14px;
+    .border-radius(5px);
+
+    &:hover {
+      background-color: #cbcbcb;
+      color: white;
+    }
+  }
+}
+
+.popover.bottom .arrow, .popover.bottom .arrow::after {
+  border-bottom-color: #333 !important;
+}
+
+#add-option-popover::before {
+  display: none;
+}
+
 .config-warning-cluster-wrapper {
   .config-warning-cluster-container {
     margin-left: -50px;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/config/components.react.jsx b/app/addons/config/components.react.jsx
new file mode 100644
index 0000000..a825282
--- /dev/null
+++ b/app/addons/config/components.react.jsx
@@ -0,0 +1,398 @@
+//  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 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.react";
+import FauxtonComponents from "../fauxton/components.react";
+
+const configStore = Stores.configStore;
+
+var ConfigTableController = React.createClass({
+  getStoreState () {
+    return {
+      options: configStore.getOptions(),
+      loading: configStore.isLoading()
+    };
+  },
+
+  getInitialState () {
+    return this.getStoreState();
+  },
+
+  componentDidMount () {
+    configStore.on('change', this.onChange, this);
+  },
+
+  componentWillUnmount () {
+    configStore.off('change', this.onChange, this);
+  },
+
+  onChange () {
+    if (this.isMounted()) {
+      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();
+  },
+
+  render () {
+    if (this.state.loading) {
+      return (
+        <div className="view">
+          <Components.LoadLines />
+        </div>
+      );
+    } else {
+      return (
+        <ConfigTable
+          onDeleteOption={this.deleteOption}
+          onSaveOption={this.saveOption}
+          onEditOption={this.editOption}
+          onCancelEdit={this.cancelEdit}
+          options={this.state.options}/>
+      );
+    }
+  }
+});
+
+var ConfigTable = React.createClass({
+  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>
+    );
+  }
+});
+
+var ConfigOption = React.createClass({
+  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>
+    );
+  }
+});
+
+var ConfigOptionValue = React.createClass({
+  getInitialState () {
+    return {
+      value: this.props.value,
+      editing: this.props.editing,
+      saving: this.props.saving
+    };
+  },
+
+  getDefaultProps () {
+    return {
+      value: '',
+      editing: false,
+      saving: false,
+      onSave: () => null,
+      onEdit: () => null,
+      onCancelEdit: () => null
+    };
+  },
+
+  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;
+    } else {
+      return (
+        <span>
+          <button
+            className="btn btn-success fonticon-ok-circled btn-small btn-config-save"
+            onClick={this.onSave}
+          />
+          <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}
+              defaultValue={this.props.value}
+              disabled={this.state.saving}
+              autoFocus type="text" className="config-value-input"
+            />
+            {this.getButtons()}
+          </div>
+        </td>
+      );
+    } else {
+      return (
+        <td className="config-show-value" onClick={this.props.onEdit}>
+          {this.props.value}
+        </td>
+      );
+    }
+  }
+});
+
+var ConfigOptionTrash = React.createClass({
+  getInitialState () {
+    return {
+      show: false
+    };
+  },
+
+  onDelete () {
+    this.props.onDelete();
+  },
+
+  showModal () {
+    this.setState({ show: true });
+  },
+
+  hideModal () {
+    this.setState({ hide: false });
+  },
+
+  render () {
+    return (
+      <td className="text-center config-item-trash config-delete-value"
+          onClick={this.showModal}>
+        <i className="icon icon-trash"></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>
+    );
+  }
+});
+
+var AddOptionController = React.createClass({
+  addOption (option) {
+    Actions.addOption(this.props.node, option);
+  },
+
+  render () {
+    return (
+      <AddOptionButton onAdd={this.addOption}/>
+    );
+  }
+});
+
+var AddOptionButton = React.createClass({
+  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}
+          type="text" name="section" placeholder="Section" autocomplete="off" autoFocus/>
+        <input
+          className="input-option-name"
+          onChange={this.updateOptionName}
+          type="text" name="name" placeholder="Name"/>
+        <input
+          className="input-value"
+          onChange={this.updateValue}
+          type="text" name="value" placeholder="Value"/>
+        <a
+          className="btn btn-create"
+          onClick={this.onAdd}>
+          Create
+        </a>
+      </Popover>
+    );
+  },
+
+  render () {
+    return (
+      <div id="add-option-panel">
+        <Button
+          id="add-option-button"
+          onClick={this.togglePopover}
+          ref="target">
+          <i className="icon icon-plus header-icon"></i>
+          Add Option
+        </Button>
+
+        <Overlay
+          show={this.state.show}
+          onHide={this.hidePopover}
+          placement="bottom"
+          rootClose={true}
+          target={() => ReactDOM.findDOMNode(this.refs.target)}>
+          {this.getPopover()}
+        </Overlay>
+      </div>
+    );
+  }
+});
+
+export default {
+  ConfigTableController: ConfigTableController,
+  ConfigTable: ConfigTable,
+  ConfigOption: ConfigOption,
+  ConfigOptionValue: ConfigOptionValue,
+  ConfigOptionTrash: ConfigOptionTrash,
+  AddOptionController: AddOptionController,
+  AddOptionButton: AddOptionButton
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/resources.js
----------------------------------------------------------------------
diff --git a/app/addons/config/resources.js b/app/addons/config/resources.js
index 87eb8c3..f010c0d 100644
--- a/app/addons/config/resources.js
+++ b/app/addons/config/resources.js
@@ -15,26 +15,21 @@ import FauxtonAPI from "../../core/api";
 
 var Config = FauxtonAPI.addon();
 
-
 Config.OptionModel = Backbone.Model.extend({
   documentation: FauxtonAPI.constants.DOC_URLS.CONFIG,
 
-  initialize: function (_, options) {
-    this.node = options.node;
-  },
-
-  url: function () {
-    if (!this.node) {
+  url () {
+    if (!this.get('node')) {
       throw new Error('no node set');
     }
 
-    return app.host + '/_node/' + this.node + '/_config/' +
-      this.get('section') + '/' + encodeURIComponent(this.get('name'));
+    return app.host + '/_node/' + this.get('node') + '/_config/' +
+      this.get('sectionName') + '/' + encodeURIComponent(this.get('optionName'));
   },
 
-  isNew: function () { return false; },
+  isNew () { return false; },
 
-  sync: function (method, model, options) {
+  sync (method, model, options) {
 
     var params = {
       url: model.url(),
@@ -52,58 +47,20 @@ Config.OptionModel = Backbone.Model.extend({
   }
 });
 
-Config.Model = Backbone.Model.extend({});
-Config.Collection = Backbone.Collection.extend({
-  model: Config.Model,
-
+Config.ConfigModel = Backbone.Model.extend({
   documentation: FauxtonAPI.constants.DOC_URLS.CONFIG,
 
-  initialize: function (_, options) {
-    this.node = options.node;
-  },
-
-  comparator: function (OptionModel) {
-    if (OptionModel.get('section')) {
-      return OptionModel.get('section');
-    }
-  },
-
-  url: function () {
-    if (!this.node) {
+  url () {
+    if (!this.get('node')) {
       throw new Error('no node set');
     }
 
-    return app.host + '/_node/' + this.node + '/_config';
+    return app.host + '/_node/' + this.get('node') + '/_config';
   },
 
-  findEntryInSection: function (sectionName, entry) {
-    var section = _.findWhere(this.toJSON(), {'section': sectionName}),
-        options;
-
-    if (!section) {
-      return false;
-    }
-
-    options = _.findWhere(section.options, {name: entry});
-
-    return options;
-  },
-
-  parse: function (resp) {
-    return _.map(resp, function (section, section_name) {
-      return {
-        section: section_name,
-        options: _.map(section, function (option, option_name) {
-          return {
-            name: option_name,
-            value: option
-          };
-        })
-      };
-    });
+  parse (resp) {
+    return { sections: resp };
   }
 });
 
-
-
 export default Config;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/routes.js
----------------------------------------------------------------------
diff --git a/app/addons/config/routes.js b/app/addons/config/routes.js
index 7d9f210..bbdfe6a 100644
--- a/app/addons/config/routes.js
+++ b/app/addons/config/routes.js
@@ -17,6 +17,8 @@ import Views from "./views";
 import CORSComponents from "../cors/components.react";
 import CORSActions from "../cors/actions";
 import ClusterActions from "../cluster/cluster.actions";
+import ConfigComponents from "./components.react";
+import ConfigActions from "./actions";
 
 
 var ConfigDisabledRouteObject = FauxtonAPI.RouteObject.extend({
@@ -58,7 +60,7 @@ var ConfigPerNodeRouteObject = FauxtonAPI.RouteObject.extend({
   initialize: function (_a, _b, options) {
     var node = options[0];
 
-    this.configs = new Config.Collection(null, {node: node});
+    this.configs = new Config.ConfigModel({ node: node });
 
     this.sidebar = this.setView('#sidebar-content', new Views.Tabs({
       sidebarItems: [
@@ -76,15 +78,17 @@ var ConfigPerNodeRouteObject = FauxtonAPI.RouteObject.extend({
     }));
   },
 
-  configForNode: function () {
-    this.newSection = this.setView('#right-header', new Views.ConfigHeader({ collection: this.configs }));
-    this.setView('#dashboard-lower-content', new Views.Table({ collection: this.configs }));
+  configForNode: function (node) {
+    this.removeComponents();
+    this.setComponent('#right-header', ConfigComponents.AddOptionController, { node });
+    this.setComponent('#dashboard-lower-content', ConfigComponents.ConfigTableController, { node });
+    ConfigActions.fetchAndEditConfig(node);
     this.sidebar.setSelectedTab('main');
   },
 
   configCorsForNode: function (node) {
-    this.removeView('#right-header');
-    this.newSection = this.setComponent('#dashboard-content', CORSComponents.CORSController);
+    this.removeComponents();
+    this.setComponent('#dashboard-lower-content', CORSComponents.CORSController);
     CORSActions.fetchAndEditCors(node);
     this.sidebar.setSelectedTab('cors');
   }

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/stores.js
----------------------------------------------------------------------
diff --git a/app/addons/config/stores.js b/app/addons/config/stores.js
new file mode 100644
index 0000000..1c112d8
--- /dev/null
+++ b/app/addons/config/stores.js
@@ -0,0 +1,149 @@
+//  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, function (section, sectionName) {
+        return {
+          sectionName,
+          options: this.mapSection(section, sectionName)
+        };
+      }.bind(this)),
+      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
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/templates/add_config_option.html
----------------------------------------------------------------------
diff --git a/app/addons/config/templates/add_config_option.html b/app/addons/config/templates/add_config_option.html
deleted file mode 100644
index 13e36bd..0000000
--- a/app/addons/config/templates/add_config_option.html
+++ /dev/null
@@ -1,26 +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.
-*/%>
-
-<a id="add-new-section" class="add-new-section-btn"><i class="icon icon-plus header-icon"></i> Add Option</a>
-
-<div class="add-section-tray tray">
-  <div class="add-on">Add Option</div>
-
-  <div class="js-form-error-config"></div>
-  <input type="text" name="section" placeholder="Section" autocomplete="off" autofocus>
-  <input type="text" name="name" placeholder="Name">
-  <input type="text" name="value" placeholder="Value">
-
-  <a class="btn" id="js-create-config-section">Create</a>
-</div>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/templates/dashboard.html
----------------------------------------------------------------------
diff --git a/app/addons/config/templates/dashboard.html b/app/addons/config/templates/dashboard.html
deleted file mode 100644
index 0af857d..0000000
--- a/app/addons/config/templates/dashboard.html
+++ /dev/null
@@ -1,24 +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.
-*/%>
-
-<table class="config table table-striped table-bordered">
-  <thead>
-    <th id="config-section" width="22%">Section</th>
-    <th id="config-option" width="20%">Option</th>
-    <th id="config-value">Value</th>
-    <th id="config-trash"></th>
-  </thead>
-  <tbody>
-  </tbody>
-</table>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/templates/header.html
----------------------------------------------------------------------
diff --git a/app/addons/config/templates/header.html b/app/addons/config/templates/header.html
deleted file mode 100644
index d2bd7fb..0000000
--- a/app/addons/config/templates/header.html
+++ /dev/null
@@ -1,15 +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.
-*/%>
-
-<div id="add-section-button" class="button"></div>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/templates/item.html
----------------------------------------------------------------------
diff --git a/app/addons/config/templates/item.html b/app/addons/config/templates/item.html
deleted file mode 100644
index 71f0a64..0000000
--- a/app/addons/config/templates/item.html
+++ /dev/null
@@ -1,40 +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.
-*/%>
-
-<% if (option.index === 0) {%>
-<th> <%- option.section %> </th>
-<% } else { %>
-<td></td>
-<% } %>
-<td class="js-edit-value">
-  <div class="js-show-value">
-    <%- option.name %>
-  </div>
-  <div class="js-edit-value-form js-hidden">
-    <input class="js-value-input" type="text" name="name" value="<%- option.name %>" />
-    <button class="js-save-value btn btn-success fonticon-ok-circled btn-small"> </button>
-    <button class="js-cancel-value btn btn-small fonticon-cancel-circled"> </button>
-  </div>
-</td>
-<td class="js-edit-value">
-  <div class="js-show-value">
-    <%- option.value %>
-  </div>
-  <div class="js-edit-value-form js-hidden">
-    <input class="js-value-input" type="text" name="value" value="<%- option.value %>" />
-    <button class="js-save-value btn btn-success fonticon-ok-circled btn-small"> </button>
-    <button class="js-cancel-value btn btn-small fonticon-cancel-circled"> </button>
-  </div>
-</td>
-<td class="js-delete-value text-center"> <i class="icon-trash"> </i> </td>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/tests/actionsSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/config/tests/actionsSpec.js b/app/addons/config/tests/actionsSpec.js
new file mode 100644
index 0000000..0d35cae
--- /dev/null
+++ b/app/addons/config/tests/actionsSpec.js
@@ -0,0 +1,193 @@
+// 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 testUtils from "../../../../test/mocha/testUtils";
+import FauxtonAPI from "../../../core/api";
+import Actions from "../actions";
+import Resources from "../resources";
+import Backbone from "backbone";
+import sinon from "sinon";
+
+const assert = testUtils.assert;
+const restore = testUtils.restore;
+
+describe('Config Actions', function () {
+  var node = 'test';
+  var option = {
+    sectionName: 'test',
+    optionName: 'test',
+    value: 'test'
+  };
+  var failXhr = { responseText: '{}' };
+
+  describe('add', function () {
+    afterEach(function () {
+      restore(Actions.optionAddSuccess);
+      restore(Actions.optionAddFailure);
+      restore(FauxtonAPI.when);
+      restore(FauxtonAPI.addNotification);
+      restore(Backbone.Model.prototype.save);
+    });
+
+    it('calls optionAddSuccess when option add succeeds', function () {
+      var stub = sinon.stub(Backbone.Model.prototype, 'save');
+      var spy = sinon.spy(Actions, 'optionAddSuccess');
+      var promise = FauxtonAPI.Deferred();
+      promise.resolve();
+      stub.returns(promise);
+
+      Actions.addOption(node, option);
+      assert.ok(spy.calledOnce);
+    });
+
+    it('shows notification when option add succeeds', function () {
+      var stub = sinon.stub(Backbone.Model.prototype, 'save');
+      var spy = sinon.spy(FauxtonAPI, 'addNotification');
+      var promise = FauxtonAPI.Deferred();
+      promise.resolve();
+      stub.returns(promise);
+
+      Actions.addOption(node, option);
+      assert.ok(spy.calledOnce);
+    });
+
+    it('calls optionAddFailure when option add fails', function () {
+      var stub = sinon.stub(Backbone.Model.prototype, 'save');
+      var spy = sinon.spy(Actions, 'optionAddFailure');
+      var promise = FauxtonAPI.Deferred();
+      promise.reject(failXhr);
+      stub.returns(promise);
+
+      Actions.addOption(node, option);
+      assert.ok(spy.calledOnce);
+    });
+
+    it('shows notification when option add fails', function () {
+      var stub = sinon.stub(Backbone.Model.prototype, 'save');
+      var spy = sinon.spy(FauxtonAPI, 'addNotification');
+      var promise = FauxtonAPI.Deferred();
+      promise.reject(failXhr);
+      stub.returns(promise);
+
+      Actions.addOption(node, option);
+      assert.ok(spy.calledOnce);
+    });
+  });
+
+  describe('save', function () {
+    afterEach(function () {
+      restore(Actions.optionSaveSuccess);
+      restore(Actions.optionSaveFailure);
+      restore(FauxtonAPI.when);
+      restore(FauxtonAPI.addNotification);
+      restore(Backbone.Model.prototype.save);
+    });
+
+    it('calls optionSaveSuccess when option save succeeds', function () {
+      var stub = sinon.stub(Backbone.Model.prototype, 'save');
+      var spy = sinon.spy(Actions, 'optionSaveSuccess');
+      var promise = FauxtonAPI.Deferred();
+      promise.resolve();
+      stub.returns(promise);
+
+      Actions.saveOption(node, option);
+      assert.ok(spy.calledOnce);
+    });
+
+    it('shows notification when option save succeeds', function () {
+      var stub = sinon.stub(Backbone.Model.prototype, 'save');
+      var spy = sinon.spy(FauxtonAPI, 'addNotification');
+      var promise = FauxtonAPI.Deferred();
+      promise.resolve();
+      stub.returns(promise);
+
+      Actions.saveOption(node, option);
+      assert.ok(spy.calledOnce);
+    });
+
+    it('calls optionSaveFailure when option save fails', function () {
+      var stub = sinon.stub(Backbone.Model.prototype, 'save');
+      var spy = sinon.spy(Actions, 'optionSaveFailure');
+      var promise = FauxtonAPI.Deferred();
+      promise.reject(failXhr);
+      stub.returns(promise);
+
+      Actions.saveOption(node, option);
+      assert.ok(spy.calledOnce);
+    });
+
+    it('shows notification when option save fails', function () {
+      var stub = sinon.stub(Backbone.Model.prototype, 'save');
+      var spy = sinon.spy(FauxtonAPI, 'addNotification');
+      var promise = FauxtonAPI.Deferred();
+      promise.reject(failXhr);
+      stub.returns(promise);
+
+      Actions.saveOption(node, option);
+      assert.ok(spy.calledOnce);
+    });
+  });
+
+  describe('delete', function () {
+    afterEach(function () {
+      restore(Actions.optionDeleteSuccess);
+      restore(Actions.optionDeleteFailure);
+      restore(FauxtonAPI.when);
+      restore(FauxtonAPI.addNotification);
+      restore(Backbone.Model.prototype.destroy);
+    });
+
+    it('calls optionDeleteSuccess when option delete succeeds', function () {
+      var stub = sinon.stub(Backbone.Model.prototype, 'destroy');
+      var spy = sinon.spy(Actions, 'optionDeleteSuccess');
+      var promise = FauxtonAPI.Deferred();
+      promise.resolve();
+      stub.returns(promise);
+
+      Actions.deleteOption(node, option);
+      assert.ok(spy.calledOnce);
+    });
+
+    it('shows notification when option delete succeeds', function () {
+      var stub = sinon.stub(Backbone.Model.prototype, 'destroy');
+      var spy = sinon.spy(FauxtonAPI, 'addNotification');
+      var promise = FauxtonAPI.Deferred();
+      promise.resolve();
+      stub.returns(promise);
+
+      Actions.deleteOption(node, option);
+      assert.ok(spy.calledOnce);
+    });
+
+    it('calls optionDeleteFailure when option delete fails', function () {
+      var stub = sinon.stub(Backbone.Model.prototype, 'destroy');
+      var spy = sinon.spy(Actions, 'optionDeleteFailure');
+      var promise = FauxtonAPI.Deferred();
+      promise.reject(failXhr);
+      stub.returns(promise);
+
+      Actions.deleteOption(node, option);
+      assert.ok(spy.calledOnce);
+    });
+
+    it('shows notification when option delete fails', function () {
+      var stub = sinon.stub(Backbone.Model.prototype, 'destroy');
+      var spy = sinon.spy(FauxtonAPI, 'addNotification');
+      var promise = FauxtonAPI.Deferred();
+      promise.reject(failXhr);
+      stub.returns(promise);
+
+      Actions.deleteOption(node, option);
+      assert.ok(spy.calledOnce);
+    });
+  });
+});
+

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/tests/componentsSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/config/tests/componentsSpec.react.jsx b/app/addons/config/tests/componentsSpec.react.jsx
new file mode 100644
index 0000000..5c49760
--- /dev/null
+++ b/app/addons/config/tests/componentsSpec.react.jsx
@@ -0,0 +1,306 @@
+// 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 Views from "../components.react";
+import Actions from "../actions";
+import Stores from "../stores";
+import utils from "../../../../test/mocha/testUtils";
+import React from "react";
+import ReactDOM from "react-dom";
+import TestUtils from "react-addons-test-utils";
+import sinon from "sinon";
+
+FauxtonAPI.router = new FauxtonAPI.Router([]);
+var assert = utils.assert;
+var configStore = Stores.configStore;
+
+describe('Config Components', function () {
+  describe('ConfigTableController', function () {
+    var container, elm, node;
+
+    beforeEach(function () {
+      container = document.createElement('div');
+      configStore._loading = false;
+      configStore._sections = {};
+      node = 'node2@127.0.0.1';
+      elm = TestUtils.renderIntoDocument(
+        <Views.ConfigTableController node={node}/>,
+        container
+      );
+    });
+
+    afterEach(function () {
+      ReactDOM.unmountComponentAtNode(container);
+    });
+
+    it('deletes options', function () {
+      var spy = sinon.stub(Actions, 'deleteOption');
+      var option = {};
+
+      elm.deleteOption(option);
+      assert.ok(spy.calledWith(node, option));
+    });
+
+    it('saves options', function () {
+      var spy = sinon.stub(Actions, 'saveOption');
+      var option = {};
+
+      elm.saveOption(option);
+      assert.ok(spy.calledWith(node, option));
+    });
+
+    it('edits options', function () {
+      var spy = sinon.stub(Actions, 'editOption');
+      var option = {};
+
+      elm.editOption(option);
+      assert.ok(spy.calledWith(option));
+    });
+
+    it('cancels editing', function () {
+      var spy = sinon.stub(Actions, 'cancelEdit');
+
+      elm.cancelEdit();
+      assert.ok(spy.calledOnce);
+    });
+  });
+
+  describe('ConfigOption', function () {
+    var container;
+
+    beforeEach(function () {
+      container = document.createElement('div');
+    });
+
+    afterEach(function () {
+      ReactDOM.unmountComponentAtNode(container);
+    });
+
+    it('renders section name if the option is a header', function () {
+      var option = {
+        sectionName: 'test_section',
+        optionName: 'test_option',
+        value: 'test_value',
+        header: true
+      };
+
+      var el = TestUtils.renderIntoDocument(<Views.ConfigOption option={option}/>, container);
+      assert.equal($(ReactDOM.findDOMNode(el)).find('th').text(), 'test_section');
+    });
+  });
+
+  describe('ConfigOptionValue', function () {
+    var container;
+
+    beforeEach(function () {
+      container = document.createElement('div');
+    });
+
+    afterEach(function () {
+      ReactDOM.unmountComponentAtNode(container);
+    });
+
+    it('displays the value prop', function () {
+      var el = TestUtils.renderIntoDocument(
+        <Views.ConfigOptionValue value={'test_value'}/>, container
+      );
+
+      assert.equal($(ReactDOM.findDOMNode(el)).text(), 'test_value');
+    });
+
+    it('starts editing when clicked', function () {
+      var spy = sinon.spy();
+      var el = TestUtils.renderIntoDocument(
+        <Views.ConfigOptionValue value={'test_value'} onEdit={spy}/>, container
+      );
+
+      TestUtils.Simulate.click($(ReactDOM.findDOMNode(el))[0]);
+      assert.ok(spy.calledOnce);
+    });
+
+    it('displays editing controls if editing', function () {
+      var el = TestUtils.renderIntoDocument(
+        <Views.ConfigOptionValue value={'test_value'} editing/>, container
+      );
+
+      assert.equal($(ReactDOM.findDOMNode(el)).find('input.config-value-input').length, 1);
+      assert.equal($(ReactDOM.findDOMNode(el)).find('button.btn-config-cancel').length, 1);
+      assert.equal($(ReactDOM.findDOMNode(el)).find('button.btn-config-save').length, 1);
+    });
+
+    it('disables input when save clicked', function () {
+      var el = TestUtils.renderIntoDocument(
+        <Views.ConfigOptionValue value={'test_value'} editing/>, container
+      );
+
+      TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button.btn-config-save')[0]);
+      assert.equal($(ReactDOM.findDOMNode(el)).find('input.config-value-input').attr('disabled'));
+    });
+
+    it('saves changed value of input when save clicked', function () {
+      var change = { target: { value: 'new_value' } };
+      var spy = sinon.spy();
+      var el = TestUtils.renderIntoDocument(
+        <Views.ConfigOptionValue value={'test_value'} editing onSave={spy}/>, container
+      );
+
+      TestUtils.Simulate.change($(ReactDOM.findDOMNode(el)).find('input.config-value-input')[0], change);
+      TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button.btn-config-save')[0]);
+      assert.ok(spy.calledWith('new_value'));
+    });
+
+    it('cancels edit if save clicked with unchanged value', function () {
+      var spy = sinon.spy();
+      var el = TestUtils.renderIntoDocument(
+        <Views.ConfigOptionValue value={'test_value'} editing onCancelEdit={spy}/>, container
+      );
+
+      TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button.btn-config-save')[0]);
+      assert.ok(spy.calledOnce);
+    });
+  });
+
+  describe('ConfigOptionTrash', function () {
+    var container;
+
+    beforeEach(function () {
+      container = document.createElement('div');
+      _.each($('div[data-reactroot]'), function (el) {
+        ReactDOM.unmountComponentAtNode(el.parentNode);
+      });
+    });
+
+    afterEach(function () {
+      ReactDOM.unmountComponentAtNode(container);
+    });
+
+    it('displays delete modal when clicked', function () {
+      var el = TestUtils.renderIntoDocument(
+        <Views.ConfigOptionTrash sectionName='test_section' optionName='test_option'/>, container
+      );
+
+      TestUtils.Simulate.click($(ReactDOM.findDOMNode(el))[0]);
+      assert.equal($('div.confirmation-modal').length, 1);
+    });
+
+    it('calls on delete when confirmation modal Okay button clicked', function () {
+      var spy = sinon.spy();
+      var el = TestUtils.renderIntoDocument(
+        <Views.ConfigOptionTrash onDelete={spy}/>, container
+      );
+
+      TestUtils.Simulate.click($(ReactDOM.findDOMNode(el))[0]);
+      TestUtils.Simulate.click($('div.confirmation-modal button.btn-success')[0]);
+      assert.ok(spy.calledOnce);
+    });
+  });
+
+  describe('AddOptionController', function () {
+    var container, elm;
+
+    beforeEach(function () {
+      container = document.createElement('div');
+      elm = TestUtils.renderIntoDocument(
+        <Views.AddOptionController node='node2@127.0.0.1'/>,
+        container
+      );
+    });
+
+    afterEach(function () {
+      ReactDOM.unmountComponentAtNode(container);
+    });
+
+    it('adds options', function () {
+      var spy = sinon.stub(Actions, 'addOption');
+
+      elm.addOption();
+      assert.ok(spy.calledOnce);
+    });
+  });
+
+  describe('AddOptionButton', function () {
+    var container;
+
+    beforeEach(function () {
+      container = document.createElement('div');
+      _.each($('div[data-reactroot]'), function (el) {
+        ReactDOM.unmountComponentAtNode(el.parentNode);
+      });
+    });
+
+    afterEach(function () {
+      ReactDOM.unmountComponentAtNode(container);
+    });
+
+    it('displays add option controls when clicked', function () {
+      var el = TestUtils.renderIntoDocument(
+        <Views.AddOptionButton/>, container
+      );
+
+      TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button#add-option-button')[0]);
+      assert.equal($('div#add-option-popover .input-section-name').length, 1);
+      assert.equal($('div#add-option-popover .input-option-name').length, 1);
+      assert.equal($('div#add-option-popover .input-value').length, 1);
+      assert.equal($('div#add-option-popover .btn-create').length, 1);
+    });
+
+    it('does not hide popover if create clicked with invalid input', function () {
+      var el = TestUtils.renderIntoDocument(
+        <Views.AddOptionButton/>, container
+      );
+
+      TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button#add-option-button')[0]);
+      TestUtils.Simulate.click($('div#add-option-popover .btn-create')[0]);
+      assert.equal($('div#add-option-popover').length, 1);
+    });
+
+    it('does not add option if create clicked with invalid input', function () {
+      var el = TestUtils.renderIntoDocument(
+        <Views.AddOptionButton/>, container
+      );
+
+      TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button#add-option-button')[0]);
+      TestUtils.Simulate.click($('div#add-option-popover .btn-create')[0]);
+      assert.equal($('div#add-option-popover').length, 1);
+    });
+
+
+    it('does adds option if create clicked with valid input', function () {
+      var el = TestUtils.renderIntoDocument(
+        <Views.AddOptionButton/>, container
+      );
+
+      TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button#add-option-button')[0]);
+      TestUtils.Simulate.click($('div#add-option-popover .btn-create')[0]);
+      assert.equal($('div#add-option-popover').length, 1);
+    });
+
+    it('adds option when create clicked with valid input', function () {
+      var spy = sinon.spy();
+      var el = TestUtils.renderIntoDocument(
+        <Views.AddOptionButton onAdd={spy}/>, container
+      );
+
+      TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button#add-option-button')[0]);
+      TestUtils.Simulate.change($('div#add-option-popover .input-section-name')[0], { target: { value: 'test_section' } });
+      TestUtils.Simulate.change($('div#add-option-popover .input-option-name')[0], { target: { value: 'test_option' } });
+      TestUtils.Simulate.change($('div#add-option-popover .input-value')[0], { target: { value: 'test_value' } });
+      TestUtils.Simulate.click($('div#add-option-popover .btn-create')[0]);
+      assert.ok(spy.calledWith(sinon.match({
+        sectionName: 'test_section',
+        optionName: 'test_option',
+        value: 'test_value'
+      })));
+    });
+  });
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/tests/configSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/config/tests/configSpec.js b/app/addons/config/tests/configSpec.js
deleted file mode 100644
index 885ba24..0000000
--- a/app/addons/config/tests/configSpec.js
+++ /dev/null
@@ -1,162 +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 Resources from "../resources";
-import Views from "../views";
-import testUtils from "../../../../test/mocha/testUtils";
-import sinon from "sinon";
-var assert = testUtils.assert,
-    ViewSandbox = testUtils.ViewSandbox,
-    collection;
-
-beforeEach(function () {
-  var optionModels = [];
-
-  _.each([1, 2, 3], function (i) {
-    var model = new Resources.OptionModel({
-      section: "foo" + i,
-      name: "bar" + i,
-      options: [{
-        name: "testname"
-      }]
-    }, {node: 'foo'});
-
-    optionModels.push(model);
-  });
-
-  collection = new Resources.Collection(optionModels, {node: 'foo'}, "foo");
-});
-
-describe("Config: Add Option Tray", function () {
-  var viewSandbox,
-      tray;
-
-  beforeEach(function (done) {
-    tray = new Views.AddConfigOptionsButton({
-      collection: collection
-    });
-
-    viewSandbox = new ViewSandbox();
-    viewSandbox.renderView(tray, done);
-  });
-
-  afterEach(function () {
-    viewSandbox.remove();
-  });
-
-  it("looks if entries are new", function () {
-    tray.$('input[name="section"]').val("foo1");
-    tray.$('input[name="name"]').val("testname");
-    assert.ok(tray.isUniqueEntryInSection(collection));
-
-    tray.$('input[name="name"]').val("testname2");
-    assert.notOk(tray.isUniqueEntryInSection(collection));
-  });
-
-  it("does not send an error for a new section", function () {
-    tray.$('input[name="section"]').val("newsection");
-    tray.$('input[name="name"]').val("testname");
-    tray.$('input[name="value"]').val("testvalue");
-    var spy = sinon.spy(tray, "showError");
-
-    tray.createConfigOption();
-    assert.notOk(spy.called);
-  });
-});
-
-describe("Config: Collection", function () {
-  it("looks if entries are new", function () {
-    assert.ok(collection.findEntryInSection("foo1", "testname"));
-    assert.notOk(collection.findEntryInSection("foo1", "testname2"));
-  });
-
-  it("returns false if findEntryInSection does not have the section", function () {
-    assert.notOk(collection.findEntryInSection("foo-not-exists", "testname"));
-  });
-});
-
-describe("Config: TableRow", function () {
-  var tabMenu, optionModel;
-
-  beforeEach(function () {
-    optionModel = new Resources.OptionModel({
-      section: "foo",
-      name: "bar"
-    }, {node: 'foo'});
-
-    tabMenu = new Views.TableRow({
-      model: optionModel,
-      uniqueName: function () {
-        return false;
-      }
-    });
-  });
-
-  describe("editing Items", function () {
-    var viewSandbox;
-    beforeEach(function (done) {
-      viewSandbox = new ViewSandbox();
-      viewSandbox.renderView(tabMenu, done);
-    });
-
-    afterEach(function () {
-      viewSandbox.remove();
-    });
-
-    it("click on save should save the model and render", function () {
-      var renderSpy = sinon.stub(tabMenu, 'render');
-      var saveSpy = sinon.stub(optionModel, 'save');
-
-      var $fields = tabMenu.$('.js-edit-value').filter(function (el) {
-        return $(this).find('[name="value"]').length;
-      });
-
-      $fields.find('.js-edit-value').trigger('dblclick');
-      $fields.find('.js-save-value').trigger('click');
-
-      assert.ok(renderSpy.calledOnce);
-      assert.ok(saveSpy.calledOnce);
-    });
-
-    it("pressing enter should save the model and render", function () {
-      var renderSpy = sinon.stub(tabMenu, 'render');
-      var saveSpy = sinon.stub(optionModel, 'save');
-
-      var e = $.Event("keyup");
-      e.keyCode = 13;
-
-      var $fields = tabMenu.$('.js-edit-value').filter(function (el) {
-        return $(this).find('[name="value"]').length;
-      });
-
-      $fields.find('.js-value-input').trigger(e);
-
-      assert.ok(renderSpy.calledOnce);
-      assert.ok(saveSpy.calledOnce);
-    });
-
-    it("pressing Esc hides the field", function () {
-      var e = $.Event("keyup");
-      e.keyCode = 27;
-      tabMenu.$('.js-value-input').trigger(e);
-
-      assert.ok(tabMenu.$('.js-edit-value-form').hasClass('js-hidden'));
-    });
-
-    it("pressing Cancel hides the field", function () {
-      tabMenu.$('.js-edit-value').trigger('dblclick');
-      tabMenu.$('.js-cancel-value').trigger('click');
-
-      assert.ok(tabMenu.$('.js-edit-value-form').hasClass('js-hidden'));
-    });
-  });
-});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/tests/storesSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/config/tests/storesSpec.js b/app/addons/config/tests/storesSpec.js
new file mode 100644
index 0000000..a36ce12
--- /dev/null
+++ b/app/addons/config/tests/storesSpec.js
@@ -0,0 +1,94 @@
+// 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';
+
+var assert = utils.assert;
+
+describe("ConfigStore", function () {
+  var configStore = Stores.configStore;
+
+  describe("mapSection", function () {
+    beforeEach(function () {
+      configStore._editOptionName = 'b';
+      configStore._editSectionName = 'test';
+    });
+
+    afterEach(function () {
+      configStore.reset();
+    });
+
+    it("sorts options ascending", function () {
+      var options = configStore.mapSection({ b: 1, c: 2, a: 3 }, 'test');
+      assert.equal(options[0].optionName, 'a');
+    });
+
+    it("sets the first option as the header", function () {
+      var options = configStore.mapSection({ b: 1, c: 2, a: 3 }, 'test');
+      assert.isTrue(options[0].header);
+    });
+
+    it("sets the option that is being edited", function () {
+      var options = configStore.mapSection({ b: 1, c: 2, a: 3 }, 'test');
+      assert.isTrue(options[1].editing);
+    });
+  });
+
+  describe("saveOption", function () {
+    var sectionName, optionName, value;
+
+    beforeEach(function () {
+      sectionName = 'a';
+      optionName = 'b';
+      value = 1;
+    });
+
+    afterEach(function () {
+      configStore.reset();
+    });
+
+    it("saves option to sections", function () {
+      configStore._sections = {};
+
+      configStore.saveOption(sectionName, optionName, value);
+      assert.deepEqual(configStore._sections, { a: { b: 1 } });
+    });
+  });
+
+  describe("deleteOption", function () {
+    var sectionName, optionName;
+
+    beforeEach(function () {
+      sectionName = 'a';
+      optionName = 'b';
+    });
+
+    afterEach(function () {
+      configStore.reset();
+    });
+
+    it("deletes option from section", function () {
+      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", function () {
+      configStore._sections = { a: { b: 1 } };
+
+      configStore.deleteOption(sectionName, optionName);
+      assert.deepEqual(configStore._sections, {});
+    });
+  });
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/views.js
----------------------------------------------------------------------
diff --git a/app/addons/config/views.js b/app/addons/config/views.js
index 398e396..c20b24a 100644
--- a/app/addons/config/views.js
+++ b/app/addons/config/views.js
@@ -10,238 +10,9 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import app from "../../app";
 import FauxtonAPI from "../../core/api";
-import Config from "./resources";
-import Components from "../fauxton/components";
 var Views = {};
 
-Views.TableRow = FauxtonAPI.View.extend({
-  tagName: "tr",
-  className: "config-item",
-  template: "addons/config/templates/item",
-  events: {
-    "dblclick .js-edit-value": "editValue",
-    "click .js-delete-value": "deleteValue",
-    "click .js-cancel-value": "cancelEdit",
-    "click .js-save-value": "saveAndRender",
-    "keyup .js-value-input": "processKeyEvents"
-  },
-
-  deleteValue: function () {
-    var collection = this.collection,
-        result = confirm("Are you sure you want to delete this configuration value?");
-
-    if (!result) { return; }
-
-    this.model.destroy().done(function () {
-      collection.fetch({reset: true}).done(function () {
-        FauxtonAPI.Events.trigger("config:rerender");
-      });
-    });
-
-    this.remove();
-  },
-
-  editValue: function (event) {
-    this.$(event.currentTarget).find(".js-show-value").addClass("js-hidden");
-    this.$(event.currentTarget).find(".js-edit-value-form").removeClass("js-hidden");
-    this.$(event.currentTarget).find(".js-value-input").focus();
-  },
-
-  processKeyEvents: function (event) {
-    // Enter key
-    if (event.keyCode === 13) {
-      return this.saveAndRender(event);
-    }
-    // Esc key
-    if (event.keyCode === 27) {
-      return this.discardValue(event);
-    }
-  },
-
-  discardValue: function (event) {
-    this.$(event.currentTarget).parents('td').find(".js-edit-value-form").addClass("js-hidden");
-    this.$(event.currentTarget).parents('td').find(".js-show-value").removeClass("js-hidden");
-  },
-
-  cancelEdit: function (event) {
-    this.discardValue(event);
-  },
-
-  serialize: function () {
-    return {option: this.model.toJSON()};
-  },
-
-  saveAndRender: function (event) {
-    var options = {},
-        $input = this.$(event.currentTarget).parents('td').find(".js-value-input"),
-        sectionName,
-        nameInSectionExists;
-
-    options[$input.attr('name')] = $input.val();
-
-    if ($input.attr('name') === 'name') {
-      sectionName = this.model.get("section");
-      nameInSectionExists = this.collection.findEntryInSection(sectionName, $input.val());
-      if (nameInSectionExists) {
-        FauxtonAPI.addNotification({
-          msg: "This config already exists, enter a unique name",
-          type: "error",
-          clear: true
-        });
-      } else {
-        var newModel = this.model.clone();
-        newModel.save(options);
-        this.model.destroy();
-        this.model = newModel;
-        this.render();
-      }
-    } else {
-      this.model.save(options);
-      this.render();
-    }
-  }
-
-});
-
-Views.Table = FauxtonAPI.View.extend({
-  template: "addons/config/templates/dashboard",
-
-  initialize: function () {
-    this.listenTo(FauxtonAPI.Events, "config:newSection", this.render);
-    this.listenTo(FauxtonAPI.Events, "config:rerender", this.render);
-  },
-
-  beforeRender: function () {
-    var collection = this.collection;
-
-    this.collection.each(function (config) {
-      _.each(config.get("options"), function (option, index) {
-        this.insertView("table.config tbody", new Views.TableRow({
-          collection: collection,
-          model: new Config.OptionModel({
-            section: config.get("section"),
-            name: option.name,
-            value: option.value,
-            index: index
-          }, {node: this.collection.node})
-        }));
-      }, this);
-    }, this);
-  },
-
-  establish: function () {
-    return [this.collection.fetch()];
-  }
-});
-
-Views.ConfigHeader = FauxtonAPI.View.extend({
-  template: 'addons/config/templates/header',
-  className: 'header-right',
-
-  initialize: function () {
-    this.rightHeader = this.setView('#add-section-button', new Views.AddConfigOptionsButton({ collection: this.collection }));
-  }
-});
-
-
-Views.AddConfigOptionsButton = Components.Tray.extend({
-  template: 'addons/config/templates/add_config_option',
-
-  events: {
-    'click #js-create-config-section': 'createConfigOption'
-  },
-
-  initialize: function () {
-    this.initTray({ toggleTrayBtnSelector: '#add-new-section' });
-  },
-
-  processKey: function (e) {
-    if (e.which === 13) {
-      e.preventDefault();
-      this.createConfigOption();
-    }
-  },
-
-  afterRender: function () {
-    this.sectionNames = _.map(this.collection.toJSON(), function (item) {
-      return item.section;
-    });
-
-    this.sectionTypeAhead = new Components.Typeahead({
-      source: this.sectionNames,
-      el: 'input[name="section"]'
-    });
-    this.sectionTypeAhead.render();
-  },
-
-  createConfigOption: function (e) {
-    if (e) {
-      e.preventDefault();
-    }
-
-    var section = this.$('input[name="section"]').val(),
-        name = this.$('input[name="name"]').val(),
-        value = this.$('input[name="value"]').val(),
-        collection = this.collection;
-
-    // perform a little validation, then submit
-    if (!section) {
-      this.showError('Please enter or select a section.');
-    } else if (!name) {
-      this.showError('Please add an option name.');
-    } else if (this.isUniqueEntryInSection(collection)) {
-      this.showError('The option have a unique name.');
-    } else if (!value) {
-      this.showError('Please add a value.');
-    } else {
-      this.submitForm();
-    }
-  },
-
-  submitForm: function () {
-
-    var option = new Config.OptionModel({
-      section: this.$('input[name="section"]').val(),
-      name: this.$('input[name="name"]').val(),
-      value: this.$('input[name="value"]').val()
-    }, {node: this.collection.node});
-    option.save();
-
-    var section = this.collection.find(function (section) {
-      return section.get('section') === option.get('section');
-    });
-
-    if (section) {
-      section.get('options').push(option.attributes);
-    } else {
-      this.collection.add({
-        section: option.get('section'),
-        options: [option.attributes]
-      });
-    }
-
-    this.hideTray();
-    FauxtonAPI.Events.trigger('config:newSection');
-  },
-
-  isUniqueEntryInSection: function (collection) {
-    var sectionName = this.$('input[name="section"]').val(),
-        entry = this.$('input[name="name"]').val();
-
-    return collection.findEntryInSection(sectionName, entry);
-  },
-
-  showError: function (msg) {
-    FauxtonAPI.addNotification({
-      msg: msg,
-      type: 'error',
-      clear: true
-    });
-  }
-});
-
 Views.Tabs = FauxtonAPI.View.extend({
   className: "sidenav",
   tagName: "nav",

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/devserver.js
----------------------------------------------------------------------
diff --git a/devserver.js b/devserver.js
index 3bef0a8..be2c9d6 100644
--- a/devserver.js
+++ b/devserver.js
@@ -1,4 +1,4 @@
-var spawn = require('child_process').spawn;
+var spawn = require('cross-spawn').spawn;
 var path = require("path");
 var fs = require("fs");
 var _ = require('lodash');
@@ -18,7 +18,7 @@ var loadSettings = function () {
     port: process.env.FAUXTON_PORT || 8000,
     contentSecurityPolicy: true,
     proxy: {
-      target: 'http://127.0.0.1:5984',
+      target: 'http://couch:5984',
       changeOrigin: false
     }
   };

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/tasks/helper.js
----------------------------------------------------------------------
diff --git a/tasks/helper.js b/tasks/helper.js
index a995c23..696e1c0 100644
--- a/tasks/helper.js
+++ b/tasks/helper.js
@@ -14,7 +14,7 @@ var fs = require('fs'),
     path = require('path');
 
 exports.devServerPort = 8000;
-exports.couch = 'http://localhost:5984/';
+exports.couch = 'http://couch:5984/';
 
 exports.init = function (grunt) {
   var _ = grunt.util._,

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/webpack.config.dev.js
----------------------------------------------------------------------
diff --git a/webpack.config.dev.js b/webpack.config.dev.js
index edb4833..05f7b92 100644
--- a/webpack.config.dev.js
+++ b/webpack.config.dev.js
@@ -73,5 +73,7 @@ module.exports = {
     path: __dirname + '/dist/debug',
     publicPath: '/',
     filename: 'bundle.js' //All our code is compiled into a single file called bundle.js
-  }
+  },
+
+  devtool: 'source-map'
 };


[2/2] fauxton commit: updated refs/heads/master to 4f633e5

Posted by ro...@apache.org.
react config: fix small issues


Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/4f633e50
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/4f633e50
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/4f633e50

Branch: refs/heads/master
Commit: 4f633e508bd9cb24cdb1a858c00d624d01732de1
Parents: ee1a92b
Author: Robert Kowalski <ro...@apache.org>
Authored: Tue Sep 13 14:31:00 2016 +0200
Committer: Robert Kowalski <ro...@apache.org>
Committed: Tue Sep 13 14:31:00 2016 +0200

----------------------------------------------------------------------
 app/addons/config/assets/less/config.less | 15 ++++++++-------
 app/addons/config/components.react.jsx    |  2 +-
 devserver.js                              |  4 ++--
 3 files changed, 11 insertions(+), 10 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/4f633e50/app/addons/config/assets/less/config.less
----------------------------------------------------------------------
diff --git a/app/addons/config/assets/less/config.less b/app/addons/config/assets/less/config.less
index f74b2ec..a9d7d7c 100644
--- a/app/addons/config/assets/less/config.less
+++ b/app/addons/config/assets/less/config.less
@@ -45,13 +45,13 @@
   background-color: #e0e0e0;
 }
 
-.table-striped tbody > tr.config-item:nth-child(odd) > td,
-.table-striped tbody > tr.config-item:nth-child(odd) > th {
+.config.table-striped tbody > tr.config-item:nth-child(odd) > td,
+.config.table-striped tbody > tr.config-item:nth-child(odd) > th {
   transition: background-color 100ms;
 }
 
-.table-striped tbody > tr.config-item:nth-child(odd):hover > td,
-.table-striped tbody > tr.config-item:nth-child(odd):hover > th {
+.config.table-striped tbody > tr.config-item:nth-child(odd):hover > td,
+.config.table-striped tbody > tr.config-item:nth-child(odd):hover > th {
   background-color: #e7e7e7;
 }
 
@@ -185,10 +185,11 @@ table.config {
       color: white;
     }
   }
-}
 
-.popover.bottom .arrow, .popover.bottom .arrow::after {
-  border-bottom-color: #333 !important;
+  .arrow, .arrow::after {
+    border-bottom-color: #333 !important;
+  }
+
 }
 
 #add-option-popover::before {

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/4f633e50/app/addons/config/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/config/components.react.jsx b/app/addons/config/components.react.jsx
index a825282..4432753 100644
--- a/app/addons/config/components.react.jsx
+++ b/app/addons/config/components.react.jsx
@@ -37,7 +37,7 @@ var ConfigTableController = React.createClass({
   },
 
   componentWillUnmount () {
-    configStore.off('change', this.onChange, this);
+    configStore.off('change', this.onChange);
   },
 
   onChange () {

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/4f633e50/devserver.js
----------------------------------------------------------------------
diff --git a/devserver.js b/devserver.js
index be2c9d6..3bef0a8 100644
--- a/devserver.js
+++ b/devserver.js
@@ -1,4 +1,4 @@
-var spawn = require('cross-spawn').spawn;
+var spawn = require('child_process').spawn;
 var path = require("path");
 var fs = require("fs");
 var _ = require('lodash');
@@ -18,7 +18,7 @@ var loadSettings = function () {
     port: process.env.FAUXTON_PORT || 8000,
     contentSecurityPolicy: true,
     proxy: {
-      target: 'http://couch:5984',
+      target: 'http://127.0.0.1:5984',
       changeOrigin: false
     }
   };