You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ac...@apache.org on 2018/06/25 13:14:58 UTC

[couchdb-fauxton] branch master updated: Migrate the CouchDB setup code to Redux (#1045)

This is an automated email from the ASF dual-hosted git repository.

acote pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/couchdb-fauxton.git


The following commit(s) were added to refs/heads/master by this push:
     new d706c95  Migrate the CouchDB setup code to Redux (#1045)
d706c95 is described below

commit d706c9583ba0f731033a25b5a7d167c4b51e6c33
Author: Alexis Côté <po...@users.noreply.github.com>
AuthorDate: Mon Jun 25 09:14:55 2018 -0400

    Migrate the CouchDB setup code to Redux (#1045)
---
 .../components/queryoptions/MainFieldsView.js      |   3 +-
 app/addons/setup/__tests__/helpers.test.js         |  70 ++++
 app/addons/setup/__tests__/setup.test.js           |  91 -----
 app/addons/setup/__tests__/setupComponents.test.js |  75 ----
 app/addons/setup/actions.js                        | 310 +++++++++++++++
 app/addons/setup/actiontypes.js                    |  23 ++
 app/addons/setup/base.js                           |  12 +
 app/addons/setup/components/ConfiguredScreen.js    |  46 +++
 .../setup/components/CurrentAdminPassword.js       |  54 +++
 app/addons/setup/components/FirstStepController.js |  71 ++++
 .../setup/components/MultipleNodeController.js     | 157 ++++++++
 app/addons/setup/components/NodeCountSetting.js    |  41 ++
 app/addons/setup/components/OptionalSettings.js    |  55 +++
 .../setup/components/SingleNodeController.js       |  80 ++++
 .../ConfiguredSceenContainer.js}                   |  20 +-
 .../{base.js => container/FirstStepContainer.js}   |  31 +-
 .../setup/container/MultipleNodeContainer.js       |  71 ++++
 app/addons/setup/container/SingleNodeContainer.js  |  60 +++
 app/addons/setup/helpers.js                        |  40 ++
 app/addons/setup/reducers.js                       | 130 +++++++
 app/addons/setup/resources.js                      |  60 ---
 app/addons/setup/route.js                          |  65 ++--
 app/addons/setup/setup.actions.js                  | 287 --------------
 app/addons/setup/setup.actiontypes.js              |  25 --
 app/addons/setup/setup.js                          | 419 ---------------------
 app/addons/setup/setup.stores.js                   | 198 ----------
 app/constants.js                                   |   1 +
 27 files changed, 1287 insertions(+), 1208 deletions(-)

diff --git a/app/addons/documents/index-results/components/queryoptions/MainFieldsView.js b/app/addons/documents/index-results/components/queryoptions/MainFieldsView.js
index a7873e4..f36f652 100644
--- a/app/addons/documents/index-results/components/queryoptions/MainFieldsView.js
+++ b/app/addons/documents/index-results/components/queryoptions/MainFieldsView.js
@@ -120,7 +120,8 @@ export default class MainFieldsView extends React.Component {
               <label htmlFor="qoStable" id="qoStableLabel">Stable</label>
             </div>
             <div className="dropdown inline">
-              <label className="drop-down">Update
+              <label className="drop-down">
+                Update
                 <select className="input-small" id="qoUpdate" value={update} onChange={this.onUpdateChange}>
                   {this.getUpdateOptions()}
                 </select>
diff --git a/app/addons/setup/__tests__/helpers.test.js b/app/addons/setup/__tests__/helpers.test.js
new file mode 100644
index 0000000..30b8bdb
--- /dev/null
+++ b/app/addons/setup/__tests__/helpers.test.js
@@ -0,0 +1,70 @@
+// 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 {isInvalid} from '../helpers';
+
+describe('Setup - Helpers', () => {
+
+  const validData = {
+    username: 'foo',
+    password: 'bar',
+    bind_address: '0.0.0.0',
+    singlenode: false,
+    port: 5984,
+    nodeCount: 3
+  };
+
+  describe('isInvalid', () => {
+
+    it('should return an error if no username is define', () => {
+      let data = Object.assign({}, validData);
+      data.username = '';
+      expect(isInvalid(data)).toBe('Admin name is required');
+    });
+
+    it('should return an error if password not set', () => {
+      let data = Object.assign({}, validData);
+      data.password = '';
+      expect(isInvalid(data)).toBe('Admin password is required');
+    });
+
+    it('should return an error if bind address is 127.0.0.1', () => {
+      let data = Object.assign({}, validData);
+      data.bind_address = '127.0.0.1';
+      expect(isInvalid(data)).toBe('Bind address can not be 127.0.0.1');
+    });
+
+    it('should return error if port is not a number', () => {
+      let data = Object.assign({}, validData);
+      data.port = 'foo';
+      expect(isInvalid(data)).toBe('Bind port must be a number');
+    });
+
+    it('should return error if node count is not a number', () => {
+
+      let data = Object.assign({}, validData);
+      data.nodeCount = 'foo';
+      expect(isInvalid(data)).toBe('Node count must be a number');
+    });
+
+    it('should return error if node counter is lower than 1', () => {
+      let data = Object.assign({}, validData);
+      data.nodeCount = 0;
+      expect(isInvalid(data)).toBe('Node count must be >= 1');
+    });
+
+    it('should return false if valid', () => {
+      expect(isInvalid(validData)).toBe(false);
+    });
+
+  });
+});
diff --git a/app/addons/setup/__tests__/setup.test.js b/app/addons/setup/__tests__/setup.test.js
deleted file mode 100644
index e22ed3a..0000000
--- a/app/addons/setup/__tests__/setup.test.js
+++ /dev/null
@@ -1,91 +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 Resources from "../resources";
-import testUtils from "../../../../test/mocha/testUtils";
-var assert = testUtils.assert,
-    model;
-
-describe('Setup: verify input', () => {
-
-  beforeEach(() => {
-    model = new Resources.Model();
-  });
-
-  it('You have to set a username', () => {
-    const error = model.validate({
-      admin: {
-        user: '',
-        password: 'ente'
-      }
-    });
-
-    assert.ok(error);
-  });
-
-  it('You have to set a password', () => {
-    const error = model.validate({
-      admin: {
-        user: 'rocko',
-        password: ''
-      }
-    });
-
-    assert.ok(error);
-  });
-
-  it('Port must be a number, if defined', () => {
-    const error = model.validate({
-      admin: {
-        user: 'rocko',
-        password: 'ente'
-      },
-      port: 'port'
-    });
-
-    assert.ok(error);
-  });
-
-  it('Bind address can not be 127.0.0.1', () => {
-    const error = model.validate({
-      admin: {
-        user: 'rocko',
-        password: 'ente'
-      },
-      bind_address: '127.0.0.1'
-    });
-
-    assert.ok(error);
-  });
-
-  it('Node count must be a number', () => {
-    const error = model.validate({
-      admin: {
-        user: 'rocko',
-        password: 'ente'
-      },
-      nodeCount: 'abc'
-    });
-    assert.ok(error);
-  });
-
-  it('Node count must be >= 1', () => {
-    const error = model.validate({
-      admin: {
-        user: 'rocko',
-        password: 'ente'
-      },
-      nodeCount: 0
-    });
-    assert.ok(error);
-  });
-
-});
diff --git a/app/addons/setup/__tests__/setupComponents.test.js b/app/addons/setup/__tests__/setupComponents.test.js
deleted file mode 100644
index 63aede2..0000000
--- a/app/addons/setup/__tests__/setupComponents.test.js
+++ /dev/null
@@ -1,75 +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 Views from "../setup";
-import Stores from "../setup.stores";
-import utils from "../../../../test/mocha/testUtils";
-import React from "react";
-import ReactDOM from "react-dom";
-import sinon from "sinon";
-import { mount } from 'enzyme';
-
-const assert = utils.assert;
-
-//this was commented out. I imagine it needs to be updated
-describe.skip('Setup Components', () => {
-
-  describe('IP / Port area', () => {
-
-    it('fires callbacks on change, ip', () => {
-      const changeHandler = sinon.spy();
-      const optSettings = mount(<Views.SetupOptionalSettings onAlterPort={null} onAlterBindAddress={changeHandler} />);
-
-      optSettings.find('.setup-input-ip').simulate('change', {target: {value: 'Hello, world'}});
-      assert.ok(changeHandler.calledOnce);
-    });
-
-    it('fires callbacks on change, port', () => {
-      const changeHandler = sinon.spy();
-      var optSettings = mount(
-        <Views.SetupOptionalSettings onAlterPort={changeHandler} onAlterBindAddress={null} />
-      );
-
-      optSettings.find('.setup-input-port').simulate('change', {target: {value: 'Hello, world'}});
-      assert.ok(changeHandler.calledOnce);
-    });
-
-  });
-
-  describe('SingleNodeSetup', () => {
-    beforeEach(() => {
-      sinon.stub(Stores.setupStore, 'getIsAdminParty', () => { return false; });
-    });
-
-    afterEach(() => {
-      utils.restore(Stores.setupStore.getIsAdminParty);
-      Stores.setupStore.reset();
-    });
-
-    it('changes the values in the store for the setup node', () => {
-      const controller = mount(
-        <Views.SetupSingleNodeController />
-      );
-
-      controller.find('.setup-setupnode-section .setup-input-ip').simulate('change', {target: {value: '192.168.13.42'}});
-      controller.find('.setup-setupnode-section .setup-input-port').simulate('change', {target: {value: '1342'}});
-      controller.find('.setup-setupnode-section .setup-username').simulate('change', {target: {value: 'tester'}});
-      controller.find('.setup-setupnode-section .setup-password').simulate('change', {target: {value: 'testerpass'}});
-
-      assert.equal(Stores.setupStore.getBindAdressForSetupNode(), '192.168.13.42');
-      assert.equal(Stores.setupStore.getPortForSetupNode(), '1342');
-      assert.equal(Stores.setupStore.getUsername(), 'tester');
-      assert.equal(Stores.setupStore.getPassword(), 'testerpass');
-    });
-
-  });
-
-});
diff --git a/app/addons/setup/actions.js b/app/addons/setup/actions.js
new file mode 100644
index 0000000..95dd352
--- /dev/null
+++ b/app/addons/setup/actions.js
@@ -0,0 +1,310 @@
+// 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 {isInvalid} from "./helpers";
+import {
+  SETUP_ADD_NODE_TO_LIST,
+  SETUP_BIND_ADDRESS_ADDITIONAL_NODE,
+  SETUP_BIND_ADDRESS_FOR_SINGLE_NODE,
+  SETUP_NODE_COUNT,
+  SETUP_PORT_ADDITIONAL_NODE,
+  SETUP_PORT_FOR_SINGLE_NODE,
+  SETUP_REMOTE_ADDRESS_ADDITIONAL_NODE,
+  SETUP_RESET_ADDITIONAL_NODE,
+  SETUP_SET_CLUSTERSTATUS,
+  SETUP_SET_PASSWORD,
+  SETUP_SET_USERNAME
+} from "./actiontypes";
+import {get, post} from '../../core/ajax';
+import Api from "../auth/api";
+
+
+/**
+ * @typedef {Object} CredentialObject
+ * @param {string} username The username
+ * @param {string} password The password
+ */
+
+
+/**
+ * Public functions
+ */
+
+const showError = (msg, error) => {
+  const errorMsg = error ? ' Error:' + error : '';
+  FauxtonAPI.addNotification({
+    msg: msg + errorMsg,
+    type: 'error',
+    fade: false,
+    clear: true
+  });
+  return;
+};
+
+
+export const getClusterStateFromCouch = () => async dispatch => {
+  const baseUrl = FauxtonAPI.urls('cluster_setup', 'server');
+  const json = await get(baseUrl);
+  dispatch({
+    type: SETUP_SET_CLUSTERSTATUS,
+    options: {
+      state: json.state
+    }
+  });
+};
+
+export const finishClusterSetup = message => async() => {
+  const baseUrl = FauxtonAPI.urls('cluster_setup', 'server');
+  const body = {action: 'finish_cluster'};
+  try {
+    const response = await post(baseUrl, body, {raw: true});
+    if (response.ok) {
+      FauxtonAPI.addNotification({
+        msg: message,
+        type: 'success',
+        fade: false,
+        clear: true
+      });
+      FauxtonAPI.navigate('#setup/finish');
+    } else {
+      const json = await response.json();
+      showError('The cluster is already finished', json.reason);
+    }
+  } catch (err) {
+    showError('There was an error. Please check your setup and try again.', err);
+  }
+};
+
+export const setupSingleNode = (credentials, setupNode) => async() => {
+  const baseUrl = FauxtonAPI.urls('cluster_setup', 'server');
+  const setupAttrs = {
+    action: 'enable_single_node',
+    username: credentials.username,
+    password: credentials.password,
+    bind_address: setupNode.bindAddress,
+    port: setupNode.port,
+    singlenode: true
+  };
+
+  const attrsAreInvalid = isInvalid(setupAttrs);
+
+  if (attrsAreInvalid) {
+    return showError(attrsAreInvalid);
+  }
+
+  try {
+    const response = await post(baseUrl, setupAttrs, {raw: true});
+    if (!response.ok) {
+      const json = await response.json();
+      const error = json.reason ? json.reason : json.error;
+      return showError(error);
+    }
+
+    await Api.login({name: credentials.username, password: credentials.password});
+    FauxtonAPI.addNotification({
+      msg: 'Single node setup successful.',
+      type: 'success',
+      fade: false,
+      clear: true
+    });
+    FauxtonAPI.navigate('#setup/finish');
+  } catch (error) {
+    showError("The cluster has not been setuped successfully.", error);
+  }
+};
+
+/**
+ * Add a node to the current cluster configuration
+ * 1. Enable cluster for the current node
+ * 2. Enable cluster for the remote node
+ * 3. Add the remote node
+ * @param isOrWasAdminParty
+ * @param credentials
+ * @param setupNode
+ * @param additionalNode
+ */
+export const addNode = (isOrWasAdminParty, credentials, setupNode, additionalNode) => async dispatch => {
+  const baseUrl = FauxtonAPI.urls('cluster_setup', 'server');
+  const enableSetupData = {
+    action: 'enable_cluster',
+    username: credentials.username,
+    password: credentials.password,
+    bind_address: setupNode.bindAddress,
+    port: setupNode.port,
+    node_count: setupNode.nodeCount,
+    singlenode: false
+  };
+
+  const attrsAreInvalid = isInvalid({
+    username: credentials.username,
+    password: credentials.password,
+    ...setupNode
+  });
+
+  if (attrsAreInvalid) {
+    return showError(attrsAreInvalid);
+  }
+
+  let enableNodeData = {
+    action: 'enable_cluster',
+    username: credentials.username,
+    password: credentials.password,
+    bind_address: additionalNode.bindAddress,
+    port: additionalNode.port,
+    node_count: setupNode.nodeCount,
+    remote_node: additionalNode.remoteAddress,
+    remote_current_user: credentials.username,
+    remote_current_password: credentials.password
+  };
+
+  if (isOrWasAdminParty) {
+    delete enableNodeData.remote_current_user;
+    delete enableNodeData.remote_current_password;
+  }
+
+  const additionalNodeDataIsInvalid = isInvalid(enableNodeData);
+
+  if (additionalNodeDataIsInvalid) {
+    return showError(additionalNodeDataIsInvalid);
+  }
+
+  const continueSetup = async() => {
+    const addNodeData = {
+      action: 'add_node',
+      username: credentials.username,
+      password: credentials.password,
+      host: additionalNode.remoteAddress,
+      port: additionalNode.port,
+      singlenode: false
+    };
+
+    //Enable the remote node
+    const enableRemoteNodeResponse = await post(baseUrl, enableNodeData, {raw: true});
+
+    if (!enableRemoteNodeResponse.ok) {
+      const json = await enableRemoteNodeResponse.json();
+      const error = json.reason ? json.reason : json.error;
+      return showError(error);
+    }
+    const addNodeResponse = await post(baseUrl, addNodeData, {raw: true});
+
+
+    if (!addNodeResponse.ok) {
+      const json = await enableRemoteNodeResponse.json();
+      const error = json.reason ? json.reason : json.error;
+      return showError(error);
+    }
+
+    dispatch({
+      type: SETUP_ADD_NODE_TO_LIST,
+      options: {
+        value: {
+          port: additionalNode.port,
+          remoteAddress: additionalNode.remoteAddress
+        }
+      }
+    });
+    FauxtonAPI.addNotification({
+      msg: 'Added node',
+      type: 'success',
+      fade: false,
+      clear: true
+    });
+  };
+  try {
+    await post(baseUrl, enableSetupData, {raw: true});
+    await Api.login({name: credentials.username, password: credentials.password});
+    await continueSetup();
+  } catch (err) {
+    showError('An error occured while adding the node.', err);
+  }
+};
+
+export const resetAddtionalNodeForm = () => {
+  return {
+    type: SETUP_RESET_ADDITIONAL_NODE,
+  };
+};
+
+export const alterPortAdditionalNode = value => {
+  return {
+    type: SETUP_PORT_ADDITIONAL_NODE,
+    options: {
+      value: value
+    }
+  };
+};
+
+export const alterRemoteAddressAdditionalNode = value => {
+  return {
+    type: SETUP_REMOTE_ADDRESS_ADDITIONAL_NODE,
+    options: {
+      value: value
+    }
+  };
+};
+
+export const alterBindAddressAdditionalNode = value => {
+  return {
+    type: SETUP_BIND_ADDRESS_ADDITIONAL_NODE,
+    options: {
+      value: value
+    }
+  };
+};
+
+export const setUsername = value => {
+  return {
+    type: SETUP_SET_USERNAME,
+    options: {
+      value: value
+    }
+  };
+};
+
+export const setPassword = value => {
+  return {
+    type: SETUP_SET_PASSWORD,
+    options: {
+      value: value
+    }
+  };
+};
+
+export const setPortForSetupNode = value => {
+  return {
+    type: SETUP_PORT_FOR_SINGLE_NODE,
+    options: {
+      value: value
+    }
+  };
+};
+
+export const setBindAddressForSetupNode = value => {
+  return {
+    type: SETUP_BIND_ADDRESS_FOR_SINGLE_NODE,
+    options: {
+      value: value
+    }
+  };
+};
+
+export const setNodeCount = value => {
+  return {
+    type: SETUP_NODE_COUNT,
+    options: {
+      value: value
+    }
+  };
+};
+
+
diff --git a/app/addons/setup/actiontypes.js b/app/addons/setup/actiontypes.js
new file mode 100644
index 0000000..cf94fd9
--- /dev/null
+++ b/app/addons/setup/actiontypes.js
@@ -0,0 +1,23 @@
+// 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 const SETUP_SET_CLUSTERSTATUS = 'SETUP_SET_CLUSTERSTATUS';
+export const SETUP_SET_USERNAME = 'SETUP_SET_USERNAME';
+export const SETUP_SET_PASSWORD = 'SETUP_SET_PASSWORD';
+export const SETUP_BIND_ADDRESS_FOR_SINGLE_NODE = 'SETUP_BIND_ADDRESS_FOR_SINGLE_NODE';
+export const SETUP_PORT_FOR_SINGLE_NODE = 'SETUP_PORT_FOR_SINGLE_NODE';
+export const SETUP_PORT_ADDITIONAL_NODE = 'SETUP_PORT_ADDITIONAL_NODE';
+export const SETUP_BIND_ADDRESS_ADDITIONAL_NODE = 'SETUP_BIND_ADDRESS_ADDITIONAL_NODE';
+export const SETUP_REMOTE_ADDRESS_ADDITIONAL_NODE = 'SETUP_REMOTE_ADDRESS_ADDITIONAL_NODE';
+export const SETUP_RESET_ADDITIONAL_NODE = 'SETUP_RESET_ADDITIONAL_NODE';
+export const SETUP_ADD_NODE_TO_LIST = 'SETUP_ADD_NODE_TO_LIST';
+export const SETUP_NODE_COUNT = 'SETUP_NODE_COUNT';
diff --git a/app/addons/setup/base.js b/app/addons/setup/base.js
index 789e52b..a457c3f 100644
--- a/app/addons/setup/base.js
+++ b/app/addons/setup/base.js
@@ -11,7 +11,9 @@
 // the License.
 
 import FauxtonAPI from "../../core/api";
+import app from '../../app';
 import Setup from "./route";
+import reducers from './reducers';
 import "./assets/less/setup.less";
 Setup.initialize = function () {
   FauxtonAPI.addHeaderLink({
@@ -21,4 +23,14 @@ Setup.initialize = function () {
   });
 };
 
+FauxtonAPI.addReducers({
+  setup: reducers
+});
+
+FauxtonAPI.registerUrls('cluster_setup', {
+  server: () => app.host + '/_cluster_setup',
+  app: () => '/_cluster_setup',
+  apiurl: () => window.location.origin + "/_cluster_setup"
+});
+
 export default Setup;
diff --git a/app/addons/setup/components/ConfiguredScreen.js b/app/addons/setup/components/ConfiguredScreen.js
new file mode 100644
index 0000000..0e7700b
--- /dev/null
+++ b/app/addons/setup/components/ConfiguredScreen.js
@@ -0,0 +1,46 @@
+// 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 PropTypes from 'prop-types';
+import app from "../../../app";
+
+
+export default class ClusterConfiguredScreen extends React.Component {
+  getNodeType = () => {
+    const {clusterState} = this.props;
+    if (clusterState === 'cluster_finished') {
+      return 'clustered';
+    } else if (clusterState === 'single_node_enabled') {
+      return 'single';
+    }
+    return 'unknown state';
+
+  };
+
+  render() {
+    const nodetype = this.getNodeType();
+
+    return (
+      <div className="setup-screen">
+        {app.i18n.en_US['couchdb-productname']} is configured for production usage as a {nodetype} node!
+        <br/>
+        <br/>
+          Do you want to <a href="#replication">replicate data</a>?
+      </div>
+    );
+  }
+}
+
+ClusterConfiguredScreen.propTypes = {
+  clusterState: PropTypes.string
+};
diff --git a/app/addons/setup/components/CurrentAdminPassword.js b/app/addons/setup/components/CurrentAdminPassword.js
new file mode 100644
index 0000000..2860013
--- /dev/null
+++ b/app/addons/setup/components/CurrentAdminPassword.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 React from 'react';
+import PropTypes from 'prop-types';
+
+
+export default class SetupCurrentAdminPassword extends React.Component {
+  render() {
+    let text = 'Specify your Admin credentials';
+
+    if (this.props.isAdminParty) {
+      text = 'Create Admin credentials.';
+    }
+
+    return (
+      <div className="setup-creds">
+        <div>
+          <p>{text}</p>
+        </div>
+        <input
+          className="setup-username"
+          onChange={this.props.onAlterUsername}
+          placeholder="Username"
+          value={this.props.username}
+          type="text"/>
+        <input
+          className="setup-password"
+          onChange={this.props.onAlterPassword}
+          placeholder="Password"
+          value={this.props.password}
+          type="password"/>
+      </div>
+    );
+  }
+}
+
+SetupCurrentAdminPassword.propTypes = {
+  onAlterUsername: PropTypes.func.isRequired,
+  onAlterPassword: PropTypes.func.isRequired,
+  username: PropTypes.string.isRequired,
+  password: PropTypes.string.isRequired,
+  isAdminParty: PropTypes.bool
+};
diff --git a/app/addons/setup/components/FirstStepController.js b/app/addons/setup/components/FirstStepController.js
new file mode 100644
index 0000000..279f382
--- /dev/null
+++ b/app/addons/setup/components/FirstStepController.js
@@ -0,0 +1,71 @@
+// 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 app from "../../../app";
+import PropTypes from 'prop-types';
+import FauxtonAPI from "../../../core/api";
+import ClusterConfiguredScreen from "./ConfiguredScreen";
+import ReactComponents from "../../components/react-components";
+
+const ConfirmButton = ReactComponents.ConfirmButton;
+
+export default class FirstStepController extends React.Component {
+
+  UNSAFE_componentWillMount() {
+    this.props.getClusterState();
+  }
+
+  render() {
+    if (this.props.clusterState === 'cluster_finished' ||
+        this.props.clusterState === 'single_node_enabled') {
+      return (<ClusterConfiguredScreen {...this.props}/>);
+    }
+
+    return (
+      <div className="setup-screen">
+        <h2>Welcome to {app.i18n.en_US['couchdb-productname']}!</h2>
+        <p>
+            This wizard should be run directly on the node, rather than through a load-balancer.
+        </p>
+        <p>
+            You can configure a single node, or a multi-node CouchDB installation.
+        </p>
+        <div>
+          <ConfirmButton
+            onClick={this.redirectToMultiNodeSetup}
+            showIcon={false}
+            text="Configure a Cluster"/>
+          <ConfirmButton
+            onClick={this.redirectToSingleNodeSetup}
+            showIcon={false}
+            id="setup-btn-no-thanks"
+            text="Configure a Single Node"/>
+        </div>
+      </div>
+    );
+  }
+
+  redirectToSingleNodeSetup = (e) => {
+    e.preventDefault();
+    FauxtonAPI.navigate('#setup/singlenode');
+  };
+
+  redirectToMultiNodeSetup = (e) => {
+    e.preventDefault();
+    FauxtonAPI.navigate('#setup/multinode');
+  };
+
+}
+
+FirstStepController.propTypes = {
+  clusterState: PropTypes.string
+};
diff --git a/app/addons/setup/components/MultipleNodeController.js b/app/addons/setup/components/MultipleNodeController.js
new file mode 100644
index 0000000..defbdec
--- /dev/null
+++ b/app/addons/setup/components/MultipleNodeController.js
@@ -0,0 +1,157 @@
+// 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 PropTypes from 'prop-types';
+import ReactComponents from "../../components/react-components";
+import CurrentAdminPassword from "./CurrentAdminPassword";
+import OptionalSettings from "./OptionalSettings";
+import NodeCountSetting from "./NodeCountSetting";
+
+import {getIsAdminParty} from '../reducers';
+
+
+const ConfirmButton = ReactComponents.ConfirmButton;
+
+export default class MultipleNodesController extends React.Component {
+
+  componentDidMount() {
+    this.isAdminParty = getIsAdminParty();
+  }
+
+  getNodeList = () => {
+    return this.props.nodeList.map(function (el, i) {
+      return (
+        <div key={i} className="node-item">
+          {el.remoteAddress}:{el.port}
+        </div>
+      );
+    }, this);
+  };
+
+  _addNode = () => {
+    const {username, password, setupNode, additionalNode} = this.props;
+    this.props.addNode(this.isAdminParty, {username, password}, setupNode, additionalNode);
+  };
+
+  _alterPortAdditionalNode = (e) => {
+    this.props.alterPortAdditionalNode(e.target.value);
+  };
+
+  _alterBindAddressAdditionalNode = (e) => {
+    this.props.alterBindAddressAdditionalNode(e.target.value);
+  };
+
+  _alterRemoteAddressAdditionalNode = (e) => {
+    this.props.alterRemoteAddressAdditionalNode(e.target.value);
+  };
+
+  _alterUsername = (e) => {
+    this.props.alterUsername(e.target.value);
+  };
+
+  _alterPassword = (e) => {
+    this.props.alterPassword(e.target.value);
+  };
+
+  _alterBindAddressSetupNode = (e) => {
+    this.props.alterBindAddressForSetupNode(e.target.value);
+  };
+
+  _alterPortSetupNode = (e) => {
+    this.props.alterPortForSetupNode(e.target.value);
+  };
+
+  _alterNodeCount = (e) => {
+    this.props.alterNodeCount(e.target.value);
+  };
+
+  _finishClusterSetup = () => {
+    this.props.finishClusterSetup('CouchDB Cluster set up!');
+  };
+
+  render() {
+    const {setupNode, additionalNode} = this.props;
+    return (
+      <div className="setup-nodes">
+          Setup your initial base-node, afterwards add the other nodes that you want to add
+        <div className="setup-setupnode-section">
+          <CurrentAdminPassword
+            {...this.props}
+            onAlterUsername={this._alterUsername}
+            onAlterPassword={this._alterPassword}/>
+
+          <OptionalSettings
+            {...this.props}
+            onAlterPort={this._alterPortSetupNode}
+            onAlterBindAddress={this._alterBindAddressSetupNode}
+            ip={setupNode.bindAddress}
+            port={setupNode.port}/>
+          <NodeCountSetting
+            {...this.props}
+            onAlterNodeCount={this._alterNodeCount}
+            nodeCount={setupNode.nodeCount}/>
+        </div>
+        <hr/>
+        <div className="setup-add-nodes-section">
+          <h2>Add Nodes to the Cluster</h2>
+          <p>Remote host</p>
+          <input
+            value={additionalNode.remoteAddress}
+            onChange={this._alterRemoteAddressAdditionalNode}
+            className="input-remote-node"
+            type="text"
+            placeholder="IP Address"/>
+          <OptionalSettings
+            {...this.props}
+            onAlterPort={this._alterPortAdditionalNode}
+            onAlterBindAddress={this._alterBindAddressAdditionalNode}
+            ip={additionalNode.bindAddress} port={additionalNode.port}/>
+
+          <div className="setup-add-button">
+            <ConfirmButton
+              onClick={this._addNode}
+              showIcon={false}
+              id="setup-btn-no-thanks"
+              text="Add Node"/>
+          </div>
+        </div>
+        <div className="setup-nodelist">
+          {this.getNodeList()}
+        </div>
+
+        <div className="centered setup-finish">
+          <ConfirmButton
+            onClick={this._finishClusterSetup}
+            showIcon={false}
+            text="Configure Cluster"/>
+        </div>
+      </div>
+    );
+  }
+}
+
+MultipleNodesController.propTypes = {
+  username: PropTypes.string.isRequired,
+  password: PropTypes.string.isRequired,
+  nodeList: PropTypes.array.isRequired,
+  isAdminParty: PropTypes.bool.isRequired,
+  setupNode: PropTypes.shape({
+    bindAddress: PropTypes.string.isRequired,
+    port: PropTypes.number.isRequired,
+    nodeCount: PropTypes.number.isRequired
+  }).isRequired,
+  additionalNode: PropTypes.shape({
+    bindAddress: PropTypes.string.isRequired,
+    port: PropTypes.number.isRequired,
+    remoteAddress: PropTypes.string.isRequired
+  }).isRequired
+};
diff --git a/app/addons/setup/components/NodeCountSetting.js b/app/addons/setup/components/NodeCountSetting.js
new file mode 100644
index 0000000..7be1e5a
--- /dev/null
+++ b/app/addons/setup/components/NodeCountSetting.js
@@ -0,0 +1,41 @@
+// 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 PropTypes from 'prop-types';
+
+
+export default class NodeCountSetting extends React.Component {
+
+  handleNodeCountChange = (event) => {
+    this.props.onAlterNodeCount(event);
+  };
+
+  render() {
+    return (
+      <div className="setup-node-count">
+        <p>Number of nodes to be added to the cluster (including this one)</p>
+        <input
+          className="setup-input-nodecount"
+          value={this.props.nodeCount}
+          onChange={this.handleNodeCountChange}
+          placeholder="Value of cluster n"
+          type="text"/>
+      </div>
+    );
+  }
+}
+
+NodeCountSetting.propTypes = {
+  onAlterNodeCount: PropTypes.func.isRequired,
+  nodeCount: PropTypes.number.isRequired
+};
diff --git a/app/addons/setup/components/OptionalSettings.js b/app/addons/setup/components/OptionalSettings.js
new file mode 100644
index 0000000..1efebb9
--- /dev/null
+++ b/app/addons/setup/components/OptionalSettings.js
@@ -0,0 +1,55 @@
+// 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 PropTypes from 'prop-types';
+
+export default class OptionalSettings extends React.Component {
+  handleIpChange = (event) => {
+    this.props.onAlterBindAddress(event);
+  };
+
+  handlePortChange = (event) => {
+    this.props.onAlterPort(event);
+  };
+
+  render() {
+    return (
+      <div className="setup-opt-settings">
+        <p>Bind address the node will listen on</p>
+        <input
+          className="setup-input-ip"
+          value={this.props.ip}
+          onChange={this.handleIpChange}
+          placeholder="IP Address"
+          type="text"/>
+
+        <div className="setup-port">
+          <p>Port that the node will use</p>
+          <input
+            className="setup-input-port"
+            value={this.props.port}
+            onChange={this.handlePortChange}
+            type="text"/>
+        </div>
+      </div>
+    );
+  }
+}
+
+OptionalSettings.propTypes = {
+  ip: PropTypes.string.isRequired,
+  port: PropTypes.number.isRequired,
+  onAlterBindAddress: PropTypes.func.isRequired,
+  onAlterPort: PropTypes.func.isRequired
+};
diff --git a/app/addons/setup/components/SingleNodeController.js b/app/addons/setup/components/SingleNodeController.js
new file mode 100644
index 0000000..29cbcfa
--- /dev/null
+++ b/app/addons/setup/components/SingleNodeController.js
@@ -0,0 +1,80 @@
+// 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 PropTypes from 'prop-types';
+import ReactComponents from "../../components/react-components";
+import CurrentAdminPassword from "./CurrentAdminPassword";
+import OptionalSettings from "./OptionalSettings";
+
+const ConfirmButton = ReactComponents.ConfirmButton;
+
+export default class SingleNodeController extends React.Component {
+
+  constructor() {
+    super();
+    this.finishSingleNode = this.finishSingleNode.bind(this);
+    this.onChangeUsername = this.onChangeUsername.bind(this);
+    this.onChangePassword = this.onChangePassword.bind(this);
+    this.onChangeBindAddress = this.onChangeBindAddress.bind(this);
+    this.onChangePort = this.onChangePort.bind(this);
+  }
+
+  onChangeUsername = e => this.props.alterUsername(e.target.value);
+
+  onChangePassword = e => this.props.alterPassword(e.target.value);
+
+  onChangeBindAddress = e => this.props.alterBindAddress(e.target.value);
+
+  onChangePort = e => this.props.alterPort(e.target.value);
+
+  render() {
+    return (
+      <div className="setup-nodes">
+        <div className="setup-setupnode-section">
+          <CurrentAdminPassword
+            {...this.props}
+            onAlterUsername={this.onChangeUsername}
+            onAlterPassword={this.onChangePassword}/>
+          <OptionalSettings
+            {...this.props}
+            onAlterPort={this.onChangePort}
+            onAlterBindAddress={this.onChangeBindAddress}
+            ip={this.props.bindAddress}
+            port={this.props.port}/>
+          <ConfirmButton
+            {...this.props}
+            onClick={this.finishSingleNode}
+            text="Configure Node"/>
+        </div>
+      </div>
+    );
+  }
+
+  finishSingleNode = (e) => {
+    e.preventDefault();
+    const {username, password, port, bindAddress} = this.props;
+    const credentials = {username, password};
+    const setupNode = {
+      port,
+      bindAddress,
+    };
+    this.props.setupSingleNode(credentials, setupNode);
+  };
+}
+
+SingleNodeController.propTypes = {
+  username: PropTypes.string.isRequired,
+  password: PropTypes.string.isRequired,
+  port: PropTypes.number.isRequired,
+  bindAddress: PropTypes.string.isRequired,
+  isAdminParty: PropTypes.bool.isRequired
+};
diff --git a/app/addons/setup/base.js b/app/addons/setup/container/ConfiguredSceenContainer.js
similarity index 64%
copy from app/addons/setup/base.js
copy to app/addons/setup/container/ConfiguredSceenContainer.js
index 789e52b..7f61f53 100644
--- a/app/addons/setup/base.js
+++ b/app/addons/setup/container/ConfiguredSceenContainer.js
@@ -9,16 +9,16 @@
 // 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 ConfiguredScreen from '../components/ConfiguredScreen';
+import {getClusterState} from '../reducers';
 
-import FauxtonAPI from "../../core/api";
-import Setup from "./route";
-import "./assets/less/setup.less";
-Setup.initialize = function () {
-  FauxtonAPI.addHeaderLink({
-    title: 'Setup',
-    href: "#/setup",
-    icon: 'fonticon-wrench'
-  });
+const mapStateToProps = ({setup}) => {
+  return {
+    clusterState: getClusterState(setup),
+  };
 };
 
-export default Setup;
+export default connect(
+  mapStateToProps
+)(ConfiguredScreen);
diff --git a/app/addons/setup/base.js b/app/addons/setup/container/FirstStepContainer.js
similarity index 51%
copy from app/addons/setup/base.js
copy to app/addons/setup/container/FirstStepContainer.js
index 789e52b..2d8a4ee 100644
--- a/app/addons/setup/base.js
+++ b/app/addons/setup/container/FirstStepContainer.js
@@ -9,16 +9,27 @@
 // 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 FirstStepController from '../components/FirstStepController';
+import {getClusterState} from '../reducers';
+import {getClusterStateFromCouch} from '../actions';
 
-import FauxtonAPI from "../../core/api";
-import Setup from "./route";
-import "./assets/less/setup.less";
-Setup.initialize = function () {
-  FauxtonAPI.addHeaderLink({
-    title: 'Setup',
-    href: "#/setup",
-    icon: 'fonticon-wrench'
-  });
+const mapStateToProps = ({setup}) => {
+  return {
+    clusterState: getClusterState(setup),
+  };
 };
 
-export default Setup;
+
+const mapDispatchToProps = (dispatch) => {
+  return {
+    getClusterState() {
+      dispatch(getClusterStateFromCouch());
+    }
+  };
+};
+
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(FirstStepController);
diff --git a/app/addons/setup/container/MultipleNodeContainer.js b/app/addons/setup/container/MultipleNodeContainer.js
new file mode 100644
index 0000000..afa05e7
--- /dev/null
+++ b/app/addons/setup/container/MultipleNodeContainer.js
@@ -0,0 +1,71 @@
+// 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 MultipleNodeController from '../components/MultipleNodeController';
+import {getNodeList, getIsAdminParty, getAdditionalNode, getUsername, getPassword, getSetupNode} from '../reducers';
+import {
+  addNode,
+  alterBindAddressAdditionalNode, alterPortAdditionalNode, alterRemoteAddressAdditionalNode, finishClusterSetup,
+  setBindAddressForSetupNode, setNodeCount, setPassword,
+  setPortForSetupNode, setUsername
+} from "../actions";
+
+const mapStateToProps = ({setup}) => {
+  return {
+    nodeList: getNodeList(setup),
+    isAdminParty: getIsAdminParty(setup),
+    setupNode: getSetupNode(setup),
+    username: getUsername(setup),
+    password: getPassword(setup),
+    additionalNode: getAdditionalNode(setup)
+  };
+};
+
+const mapDispatchToProps = (dispatch) => {
+  return {
+    addNode(isAdminParty, credentials, setupNode, additionalNode) {
+      dispatch(addNode(isAdminParty, credentials, setupNode, additionalNode));
+    },
+    alterPortAdditionalNode(port) {
+      dispatch(alterPortAdditionalNode(port));
+    },
+    alterBindAddressAdditionalNode(bindAddress) {
+      dispatch(alterBindAddressAdditionalNode(bindAddress));
+    },
+    alterRemoteAddressAdditionalNode(remoteAddress) {
+      dispatch(alterRemoteAddressAdditionalNode(remoteAddress));
+    },
+    alterUsername(username) {
+      dispatch(setUsername(username));
+    },
+    alterPassword(password) {
+      dispatch(setPassword(password));
+    },
+    alterBindAddressForSetupNode(bindAddress) {
+      dispatch(setBindAddressForSetupNode(bindAddress));
+    },
+    alterPortForSetupNode(port) {
+      dispatch(setPortForSetupNode(port));
+    },
+    finishClusterSetup(msg) {
+      dispatch(finishClusterSetup(msg));
+    },
+    alterNodeCount(nodeCount) {
+      dispatch(setNodeCount(nodeCount));
+    }
+  };
+};
+
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(MultipleNodeController);
diff --git a/app/addons/setup/container/SingleNodeContainer.js b/app/addons/setup/container/SingleNodeContainer.js
new file mode 100644
index 0000000..b70de41
--- /dev/null
+++ b/app/addons/setup/container/SingleNodeContainer.js
@@ -0,0 +1,60 @@
+// 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 SingleNodeController from '../components/SingleNodeController';
+import {
+  getIsAdminParty,
+  getBindAddressForSetupNode,
+  getPortForSetupNode,
+  getNodeList,
+  getPassword,
+  getSetupNode,
+  getUsername
+} from '../reducers';
+import {setBindAddressForSetupNode, setPassword, setPortForSetupNode, setupSingleNode, setUsername} from "../actions";
+
+const mapStateToProps = ({setup}) => {
+  return {
+    nodeList: getNodeList(setup),
+    isAdminParty: getIsAdminParty(setup),
+    setupNode: getSetupNode(setup),
+    username: getUsername(setup),
+    password: getPassword(setup),
+    port: getPortForSetupNode(setup),
+    bindAddress: getBindAddressForSetupNode(setup)
+  };
+};
+
+const mapDispatchToProps = dispatch => {
+  return {
+    alterUsername(username) {
+      dispatch(setUsername(username));
+    },
+    alterPassword(password) {
+      dispatch(setPassword(password));
+    },
+    alterBindAddress(bindAddress) {
+      dispatch(setBindAddressForSetupNode(bindAddress));
+    },
+    alterPort(port) {
+      dispatch(setPortForSetupNode(port));
+    },
+    setupSingleNode(credentials, setupNode) {
+      dispatch(setupSingleNode(credentials, setupNode));
+    }
+  };
+};
+
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(SingleNodeController);
diff --git a/app/addons/setup/helpers.js b/app/addons/setup/helpers.js
new file mode 100644
index 0000000..27fde8f
--- /dev/null
+++ b/app/addons/setup/helpers.js
@@ -0,0 +1,40 @@
+// 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 _ from 'lodash';
+
+export function isInvalid(attrs) {
+  if (_.isEmpty(attrs.username)) {
+    return 'Admin name is required';
+  }
+
+  if (_.isEmpty(attrs.password)) {
+    return 'Admin password is required';
+  }
+
+  if (attrs.bind_address && attrs.bind_address === '127.0.0.1' &&
+        !attrs.singlenode) {
+    return 'Bind address can not be 127.0.0.1';
+  }
+
+  if (attrs.port && _.isNaN(+attrs.port)) {
+    return 'Bind port must be a number';
+  }
+
+  if (attrs.nodeCount && _.isNaN(+attrs.nodeCount)) {
+    return 'Node count must be a number';
+  }
+
+  if (attrs.nodeCount === 0 || attrs.nodeCount && attrs.nodeCount < 1) {
+    return 'Node count must be >= 1';
+  }
+  return false;
+}
diff --git a/app/addons/setup/reducers.js b/app/addons/setup/reducers.js
new file mode 100644
index 0000000..203949d
--- /dev/null
+++ b/app/addons/setup/reducers.js
@@ -0,0 +1,130 @@
+// 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 {
+  SETUP_SET_CLUSTERSTATUS,
+  SETUP_SET_USERNAME,
+  SETUP_SET_PASSWORD,
+  SETUP_BIND_ADDRESS_FOR_SINGLE_NODE,
+  SETUP_PORT_FOR_SINGLE_NODE,
+  SETUP_PORT_ADDITIONAL_NODE,
+  SETUP_BIND_ADDRESS_ADDITIONAL_NODE,
+  SETUP_REMOTE_ADDRESS_ADDITIONAL_NODE,
+  SETUP_ADD_NODE_TO_LIST,
+  SETUP_RESET_ADDITIONAL_NODE,
+  SETUP_NODE_COUNT
+} from './actiontypes';
+import FauxtonAPI from "../../core/api";
+import _ from "lodash";
+
+
+const initialState = {
+  clusterState: '',
+  username: '',
+  password: '',
+  setupNode: {
+    bindAddress: '0.0.0.0',
+    port: 5984,
+    nodeCount: 3
+  },
+  nodeList: [],
+  additionalNode: {
+    bindAddress: '0.0.0.0',
+    port: 5984,
+    remoteAddress: '127.0.0.1'
+  }
+};
+
+export default function setup(state = initialState, action) {
+  const {options, type} = action;
+  switch (type) {
+    case SETUP_SET_CLUSTERSTATUS:
+      return updateState(state, 'clusterState', options.state);
+    case SETUP_SET_USERNAME:
+      return updateState(state, 'username', options.value);
+    case SETUP_SET_PASSWORD:
+      return updateState(state, 'password', options.value);
+    case SETUP_BIND_ADDRESS_FOR_SINGLE_NODE:
+      return updateState(state, 'setupNode.bindAddress', options.value);
+    case SETUP_PORT_FOR_SINGLE_NODE:
+      return updateState(state, 'setupNode.port', options.value);
+    case SETUP_PORT_ADDITIONAL_NODE:
+      return updateState(state, 'additionalNode.port', parseInt(options.value));
+    case SETUP_BIND_ADDRESS_ADDITIONAL_NODE:
+      return updateState(state, 'additionalNode.bindAddress', options.value);
+    case SETUP_REMOTE_ADDRESS_ADDITIONAL_NODE:
+      return updateState(state, 'additionalNode.remoteAddress', options.value);
+    case SETUP_ADD_NODE_TO_LIST:
+      let addNodeListState = getStateCopy(state);
+      addNodeListState.nodeList.push(options.value);
+      resetAdditionalNode(addNodeListState);
+      return addNodeListState;
+    case SETUP_RESET_ADDITIONAL_NODE:
+      return resetAdditionalNode(getStateCopy(state));
+    case SETUP_NODE_COUNT:
+      const nodeCount = Math.min(options.value, 3);
+      return updateState(state, 'setupNode.nodeCount', nodeCount);
+    default:
+      return state;
+  }
+}
+
+/**
+ * Manual nested copy of the state object.
+ * @param state The current state to copy.
+ * @returns {{setupNode: {}, additionalNode: {}}}
+ */
+export const getStateCopy = (state) => {
+  return {
+    ...state,
+    setupNode: {
+      ...state.setupNode
+    },
+    additionalNode: {
+      ...state.additionalNode
+    }
+  };
+};
+
+/**
+ * Update a particular value for a state
+ * @param state The state to update
+ * @param path The property path to update
+ * @param value The value to update
+ */
+const updateState = (state, path, value) => {
+  let statecopy = getStateCopy(state);
+  return _.set(statecopy, path, value);
+};
+
+/**
+ * Reset the current additionalNode state for the initial one.
+ * @param state The state to update
+ * @returns {*}
+ */
+const resetAdditionalNode = state => {
+  state.additionalNode = Object.assign({}, initialState.additionalNode);
+  return state;
+};
+
+export const getState = state => state;
+export const getClusterState = state => state.clusterState;
+export const getNodeList = state => state.nodeList;
+export const getIsAdminParty = () => FauxtonAPI.session.isAdminParty();
+export const getUsername = state => state.username;
+export const getPassword = state => state.password;
+export const getSetupNode = state => state.setupNode;
+export const getPortForSetupNode = state => state.setupNode.port;
+export const getBindAddressForSetupNode = state => state.setupNode.bindAddress;
+export const getNodeCountForSetupNode = state => state.setupNode.nodeCount;
+export const getAdditionalNode = state => state.additionalNode;
+export const getHostForSetupNode = () => '127.0.0.1';
diff --git a/app/addons/setup/resources.js b/app/addons/setup/resources.js
deleted file mode 100644
index 3f91a3e..0000000
--- a/app/addons/setup/resources.js
+++ /dev/null
@@ -1,60 +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 app from "../../app";
-import FauxtonAPI from "../../core/api";
-
-var Setup = FauxtonAPI.addon();
-
-
-Setup.Model = Backbone.Model.extend({
-
-  documentation: app.host + '/_utils/docs',
-
-  url: function (context) {
-    if (context === "apiurl") {
-      return window.location.origin + "/_cluster_setup";
-    }
-    return '/_cluster_setup';
-
-  },
-
-  validate: function (attrs) {
-    if (!attrs.username) {
-      return 'Admin name is required';
-    }
-
-    if (!attrs.password) {
-      return 'Admin password is required';
-    }
-
-    if (attrs.bind_address && attrs.bind_address === '127.0.0.1' &&
-        !attrs.singlenode) {
-      return 'Bind address can not be 127.0.0.1';
-    }
-
-    if (attrs.port && _.isNaN(+attrs.port)) {
-      return 'Bind port must be a number';
-    }
-
-    if (attrs.nodeCount && _.isNaN(+attrs.nodeCount)) {
-      return 'Node count must be a number';
-    }
-
-    if (attrs.nodeCount && attrs.nodeCount < 1) {
-      return 'Node count must be >= 1';
-    }
-  }
-
-});
-
-export default Setup;
diff --git a/app/addons/setup/route.js b/app/addons/setup/route.js
index b8845bb..945f3e4 100644
--- a/app/addons/setup/route.js
+++ b/app/addons/setup/route.js
@@ -13,13 +13,15 @@
 import React from 'react';
 import app from "../../app";
 import FauxtonAPI from "../../core/api";
-import Setup from "./resources";
-import SetupComponents from "./setup";
-import SetupActions from "./setup.actions";
 import ClusterActions from "../cluster/cluster.actions";
 import {OnePaneSimpleLayout} from '../components/layouts';
 
-var RouteObject = FauxtonAPI.RouteObject.extend({
+import ConfiguredScreenContainer from './container/ConfiguredSceenContainer';
+import FirstStepContainer from './container/FirstStepContainer';
+import SingleNodeContainer from './container/SingleNodeContainer';
+import MultipleNodeContainer from './container/MultipleNodeContainer';
+
+const SetupRouteObject = FauxtonAPI.RouteObject.extend({
   roles: ['_admin'],
   selectedHeader: 'Setup',
 
@@ -30,61 +32,60 @@ var RouteObject = FauxtonAPI.RouteObject.extend({
     'setup/multinode': 'setupMultiNode'
   },
 
-  setupInitView: function () {
-    const setup = new Setup.Model();
+  setupInitView: () => {
+    const url = FauxtonAPI.urls('cluster_setup', 'apiurl');
     ClusterActions.fetchNodes();
-    SetupActions.getClusterStateFromCouch();
     return <OnePaneSimpleLayout
-      component={<SetupComponents.SetupFirstStepController/>}
-      endpoint={setup.url('apiurl')}
-      docURL={setup.documentation}
+      component={<FirstStepContainer />}
+      endpoint={url}
+      docURL={FauxtonAPI.constants.DOC_URLS.SETUP}
       crumbs={[
-        { 'name': 'Setup ' + app.i18n.en_US['couchdb-productname'] }
+        {'name': 'Setup ' + app.i18n.en_US['couchdb-productname']}
       ]}
     />;
   },
 
-  setupSingleNode: function () {
-    const setup = new Setup.Model();
+  setupSingleNode: () => {
+    const url = FauxtonAPI.urls('cluster_setup', 'apiurl');
     ClusterActions.fetchNodes();
     return <OnePaneSimpleLayout
-      component={<SetupComponents.SetupSingleNodeController/>}
-      endpoint={setup.url('apiurl')}
-      docURL={setup.documentation}
+      component={<SingleNodeContainer />}
+      endpoint={url}
+      docURL={FauxtonAPI.constants.DOC_URLS.SETUP}
       crumbs={[
-        { 'name': 'Setup ' + app.i18n.en_US['couchdb-productname'] }
+        {'name': 'Setup ' + app.i18n.en_US['couchdb-productname']}
       ]}
     />;
   },
 
-  setupMultiNode: function () {
-    const setup = new Setup.Model();
+  setupMultiNode: () => {
+    const url = FauxtonAPI.urls('cluster_setup', 'apiurl');
     ClusterActions.fetchNodes();
     return <OnePaneSimpleLayout
-      component={<SetupComponents.SetupMultipleNodesController/>}
-      endpoint={setup.url('apiurl')}
-      docURL={setup.documentation}
+      component={<MultipleNodeContainer />}
+      endpoint={url}
+      docURL={FauxtonAPI.constants.DOC_URLS.SETUP}
       crumbs={[
-        { 'name': 'Setup ' + app.i18n.en_US['couchdb-productname'] }
+        {'name': 'Setup ' + app.i18n.en_US['couchdb-productname']}
       ]}
     />;
   },
 
-  finishView: function () {
-    const setup = new Setup.Model();
-    SetupActions.getClusterStateFromCouch();
+  finishView: () => {
+    const url = FauxtonAPI.urls('cluster_setup', 'apiurl');
     return <OnePaneSimpleLayout
-      component={<SetupComponents.ClusterConfiguredScreen/>}
-      endpoint={setup.url('apiurl')}
-      docURL={setup.documentation}
+      component={<ConfiguredScreenContainer/>}
+      endpoint={url}
+      docURL={FauxtonAPI.constants.DOC_URLS.SETUP}
       crumbs={[
-        { 'name': 'Setup ' + app.i18n.en_US['couchdb-productname'] }
+        {'name': 'Setup ' + app.i18n.en_US['couchdb-productname']}
       ]}
     />;
   }
 });
 
-
-Setup.RouteObjects = [RouteObject];
+const Setup = {
+  RouteObjects: [SetupRouteObject]
+};
 
 export default Setup;
diff --git a/app/addons/setup/setup.actions.js b/app/addons/setup/setup.actions.js
deleted file mode 100644
index 9d5209a..0000000
--- a/app/addons/setup/setup.actions.js
+++ /dev/null
@@ -1,287 +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 SetupResources from "./resources";
-import ActionTypes from "./setup.actiontypes";
-import SetupStores from "./setup.stores";
-import Api from "../auth/api";
-var setupStore = SetupStores.setupStore;
-
-export default {
-
-  getClusterStateFromCouch: function () {
-    var setupData = new SetupResources.Model();
-
-    setupData.fetch().then(function () {
-      FauxtonAPI.dispatch({
-        type: ActionTypes.SETUP_SET_CLUSTERSTATUS,
-        options: {
-          state: setupData.get('state')
-        }
-      });
-    });
-  },
-
-  finishClusterSetup: function (message) {
-
-    $.ajax({
-      type: 'POST',
-      url: '/_cluster_setup',
-      contentType: 'application/json',
-      dataType: 'json',
-      data: JSON.stringify({
-        action: 'finish_cluster'
-      })
-    }).success(function () {
-      FauxtonAPI.addNotification({
-        msg: message,
-        type: 'success',
-        fade: false,
-        clear: true
-      });
-      FauxtonAPI.navigate('#setup/finish');
-    }).fail(function () {
-      FauxtonAPI.addNotification({
-        msg: 'There was an error. Please check your setup and try again.',
-        type: 'error',
-        fade: false,
-        clear: true
-      });
-    });
-
-  },
-
-  setupSingleNode: function () {
-    var username = setupStore.getUsername();
-    var password = setupStore.getPassword();
-
-    var setupModel = new SetupResources.Model({
-      action: 'enable_single_node',
-      username: username,
-      password: password,
-      bind_address: setupStore.getBindAdressForSetupNode(),
-      port: setupStore.getPortForSetupNode(),
-      singlenode: true
-    });
-
-    setupModel.on('invalid', function (model, error) {
-      FauxtonAPI.addNotification({
-        msg: error,
-        type: 'error',
-        fade: false,
-        clear: true
-      });
-    });
-
-    setupModel.save()
-      .then(function () {
-        return Api.login({name: username, password});
-      })
-      .then(function () {
-        FauxtonAPI.addNotification({
-          msg: 'Single node setup successful.',
-          type: 'success',
-          fade: false,
-          clear: true
-        });
-        FauxtonAPI.navigate('#setup/finish');
-      }.bind(this));
-  },
-
-  addNode: function (isOrWasAdminParty) {
-    var username = setupStore.getUsername();
-    var password = setupStore.getPassword();
-    var portForSetupNode = setupStore.getPortForSetupNode();
-    var bindAddressForSetupNode = setupStore.getBindAdressForSetupNode();
-    var nodeCountForSetupNode = setupStore.getNodeCountForSetupNode();
-
-    var bindAddressForAdditionalNode = setupStore.getAdditionalNode().bindAddress;
-    var remoteAddressForAdditionalNode = setupStore.getAdditionalNode().remoteAddress;
-    var portForForAdditionalNode = setupStore.getAdditionalNode().port;
-
-
-    var setupNode = new SetupResources.Model({
-      action: 'enable_cluster',
-      username: username,
-      password: password,
-      bind_address: bindAddressForSetupNode,
-      port: portForSetupNode,
-      node_count: nodeCountForSetupNode,
-      singlenode: false
-    });
-
-    setupNode.on('invalid', function (model, error) {
-      FauxtonAPI.addNotification({
-        msg: error,
-        type: 'error',
-        fade: false,
-        clear: true
-      });
-    });
-
-    var additionalNodeData = {
-      action: 'enable_cluster',
-      username: username,
-      password: password,
-      bind_address: bindAddressForAdditionalNode,
-      port: portForForAdditionalNode,
-      node_count: nodeCountForSetupNode,
-      remote_node: remoteAddressForAdditionalNode,
-      remote_current_user: username,
-      remote_current_password: password
-    };
-
-    if (isOrWasAdminParty) {
-      delete additionalNodeData.remote_current_user;
-      delete additionalNodeData.remote_current_password;
-    }
-
-    var additionalNode = new SetupResources.Model(additionalNodeData);
-
-    additionalNode.on('invalid', function (model, error) {
-      FauxtonAPI.addNotification({
-        msg: error,
-        type: 'error',
-        fade: false,
-        clear: true
-      });
-    });
-    setupNode
-      .save()
-      .always(function () {
-        Api.login({name: username, password}).then(function() {
-          continueSetup();
-        });
-      });
-
-    function continueSetup () {
-      var addNodeModel = new SetupResources.Model({
-        action: 'add_node',
-        username: username,
-        password: password,
-        host: remoteAddressForAdditionalNode,
-        port: portForForAdditionalNode,
-        singlenode: false
-      });
-
-      additionalNode
-        .save()
-        .then(function () {
-          return addNodeModel.save();
-        })
-        .then(function () {
-          FauxtonAPI.dispatch({
-            type: ActionTypes.SETUP_ADD_NODE_TO_LIST,
-            options: {
-              value: {
-                port: portForForAdditionalNode,
-                remoteAddress: remoteAddressForAdditionalNode
-              }
-            }
-          });
-          FauxtonAPI.addNotification({
-            msg: 'Added node',
-            type: 'success',
-            fade: false,
-            clear: true
-          });
-        })
-        .fail(function (xhr) {
-          var responseText = JSON.parse(xhr.responseText).reason;
-          FauxtonAPI.addNotification({
-            msg: 'Adding node failed: ' + responseText,
-            type: 'error',
-            fade: false,
-            clear: true
-          });
-        });
-    }
-  },
-
-  resetAddtionalNodeForm: function () {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.SETUP_RESET_ADDITIONAL_NODE,
-    });
-  },
-
-  alterPortAdditionalNode: function (value) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.SETUP_PORT_ADDITIONAL_NODE,
-      options: {
-        value: value
-      }
-    });
-  },
-
-  alterRemoteAddressAdditionalNode: function (value) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.SETUP_REMOTE_ADDRESS_ADDITIONAL_NODE,
-      options: {
-        value: value
-      }
-    });
-  },
-
-  alterBindAddressAdditionalNode: function (value) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.SETUP_BIND_ADDRESS_ADDITIONAL_NODE,
-      options: {
-        value: value
-      }
-    });
-  },
-
-  setUsername: function (value) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.SETUP_SET_USERNAME,
-      options: {
-        value: value
-      }
-    });
-  },
-
-  setPassword: function (value) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.SETUP_SET_PASSWORD,
-      options: {
-        value: value
-      }
-    });
-  },
-
-  setPortForSetupNode: function (value) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.SETUP_PORT_FOR_SINGLE_NODE,
-      options: {
-        value: value
-      }
-    });
-  },
-
-  setBindAddressForSetupNode: function (value) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.SETUP_BIND_ADDRESS_FOR_SINGLE_NODE,
-      options: {
-        value: value
-      }
-    });
-  },
-
-  setNodeCount: function (value) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.SETUP_NODE_COUNT,
-      options: {
-        value: value
-      }
-    });
-  }
-};
diff --git a/app/addons/setup/setup.actiontypes.js b/app/addons/setup/setup.actiontypes.js
deleted file mode 100644
index 0353cc2..0000000
--- a/app/addons/setup/setup.actiontypes.js
+++ /dev/null
@@ -1,25 +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.
-
-export default {
-  SETUP_SET_CLUSTERSTATUS: 'SETUP_SET_CLUSTERSTATUS',
-  SETUP_SET_USERNAME: 'SETUP_SET_USERNAME',
-  SETUP_SET_PASSWORD: 'SETUP_SET_PASSWORD',
-  SETUP_BIND_ADDRESS_FOR_SINGLE_NODE: 'SETUP_BIND_ADDRESS_FOR_SINGLE_NODE',
-  SETUP_PORT_FOR_SINGLE_NODE: 'SETUP_PORT_FOR_SINGLE_NODE',
-  SETUP_PORT_ADDITIONAL_NODE: 'SETUP_PORT_ADDITIONAL_NODE',
-  SETUP_BIND_ADDRESS_ADDITIONAL_NODE: 'SETUP_BIND_ADDRESS_ADDITIONAL_NODE',
-  SETUP_REMOTE_ADDRESS_ADDITIONAL_NODE: 'SETUP_REMOTE_ADDRESS_ADDITIONAL_NODE',
-  SETUP_RESET_ADDITIONAL_NODE: 'SETUP_RESET_ADDITIONAL_NODE',
-  SETUP_ADD_NODE_TO_LIST: 'SETUP_ADD_NODE_TO_LIST',
-  SETUP_NODE_COUNT: 'SETUP_NODE_COUNT',
-};
diff --git a/app/addons/setup/setup.js b/app/addons/setup/setup.js
deleted file mode 100644
index fe1a59c..0000000
--- a/app/addons/setup/setup.js
+++ /dev/null
@@ -1,419 +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 app from "../../app";
-import FauxtonAPI from "../../core/api";
-import React from "react";
-import ReactComponents from "../components/react-components";
-import SetupActions from "./setup.actions";
-import SetupStores from "./setup.stores";
-
-var setupStore = SetupStores.setupStore;
-var ConfirmButton = ReactComponents.ConfirmButton;
-
-
-class ClusterConfiguredScreen extends React.Component {
-  getStoreState = () => {
-    return {
-      clusterState: setupStore.getClusterState()
-    };
-  };
-
-  componentDidMount() {
-    setupStore.on('change', this.onChange, this);
-  }
-
-  componentWillUnmount() {
-    setupStore.off('change', this.onChange);
-  }
-
-  onChange = () => {
-    this.setState(this.getStoreState());
-  };
-
-  getNodeType = () => {
-    if (this.state.clusterState === 'cluster_finished') {
-      return 'clustered';
-    } else if (this.state.clusterState === 'single_node_enabled') {
-      return 'single';
-    }
-    return 'unknown state';
-
-  };
-
-  state = this.getStoreState();
-
-  render() {
-    var nodetype = this.getNodeType();
-
-    return (
-      <div className="setup-screen">
-        {app.i18n.en_US['couchdb-productname']} is configured for production usage as a {nodetype} node!
-        <br />
-        <br/>
-        Do you want to <a href="#replication">replicate data</a>?
-      </div>
-    );
-  }
-}
-
-class SetupCurrentAdminPassword extends React.Component {
-  render() {
-    var text = 'Specify your Admin credentials';
-
-    if (this.props.adminParty) {
-      text = 'Create Admin credentials.';
-    }
-
-    return (
-      <div className="setup-creds">
-        <div>
-          <p>{text}</p>
-        </div>
-        <input
-          className="setup-username"
-          onChange={this.props.onAlterUsername}
-          placeholder="Username"
-          type="text" />
-        <input
-          className="setup-password"
-          onChange={this.props.onAlterPassword}
-          placeholder="Password"
-          type="password" />
-      </div>
-    );
-  }
-}
-
-class SetupNodeCountSetting extends React.Component {
-  state = {
-    nodeCountValue: this.props.nodeCountValue
-  };
-
-  handleNodeCountChange = (event) => {
-    this.props.onAlterNodeCount(event);
-    this.setState({nodeCountValue: event.target.value});
-  };
-
-  render() {
-    return (
-      <div className="setup-node-count">
-        <p>Number of nodes to be added to the cluster (including this one)</p>
-        <input
-          className="setup-input-nodecount"
-          value={this.state.nodeCountValue}
-          onChange={this.handleNodeCountChange}
-          placeholder="Value of cluster n"
-          type="text" />
-      </div>
-    );
-  }
-}
-
-class SetupOptionalSettings extends React.Component {
-  state = {
-    ipValue: this.props.ipInitialValue,
-    portValue: this.props.portValue
-  };
-
-  handleIpChange = (event) => {
-    this.props.onAlterBindAddress(event);
-    this.setState({ipValue: event.target.value});
-  };
-
-  handlePortChange = (event) => {
-    this.props.onAlterPort(event);
-    this.setState({portValue: event.target.value});
-  };
-
-  render() {
-    return (
-      <div className="setup-opt-settings">
-        <p>Bind address the node will listen on</p>
-        <input
-          className="setup-input-ip"
-          value={this.state.ipValue}
-          onChange={this.handleIpChange}
-          placeholder="IP Address"
-          type="text" />
-
-        <div className="setup-port">
-          <p>Port that the node will use</p>
-          <input
-            className="setup-input-port"
-            value={this.state.portValue}
-            onChange={this.handlePortChange}
-            defaultValue="5984"
-            type="text" />
-        </div>
-      </div>
-    );
-  }
-}
-
-class SetupMultipleNodesController extends React.Component {
-  getStoreState = () => {
-    return {
-      nodeList: setupStore.getNodeList(),
-      isAdminParty: setupStore.getIsAdminParty(),
-      remoteAddress: setupStore.getAdditionalNode().remoteAddress
-    };
-  };
-
-  componentDidMount() {
-    this.isAdminParty = setupStore.getIsAdminParty();
-    setupStore.on('change', this.onChange, this);
-  }
-
-  componentWillUnmount() {
-    setupStore.off('change', this.onChange);
-  }
-
-  onChange = () => {
-    this.setState(this.getStoreState());
-  };
-
-  getNodeList = () => {
-    return this.state.nodeList.map(function (el, i) {
-      return (
-        <div key={i} className="node-item">
-          {el.remoteAddress}:{el.port}
-        </div>
-      );
-    }, this);
-  };
-
-  addNode = () => {
-    SetupActions.addNode(this.isAdminParty);
-  };
-
-  alterPortAdditionalNode = (e) => {
-    SetupActions.alterPortAdditionalNode(e.target.value);
-  };
-
-  alterBindAddressAdditionalNode = (e) => {
-    SetupActions.alterBindAddressAdditionalNode(e.target.value);
-  };
-
-  alterRemoteAddressAdditionalNode = (e) => {
-    SetupActions.alterRemoteAddressAdditionalNode(e.target.value);
-  };
-
-  alterUsername = (e) => {
-    SetupActions.setUsername(e.target.value);
-  };
-
-  alterPassword = (e) => {
-    SetupActions.setPassword(e.target.value);
-  };
-
-  alterBindAddressSetupNode = (e) => {
-    SetupActions.setBindAddressForSetupNode(e.target.value);
-  };
-
-  alterPortSetupNode = (e) => {
-    SetupActions.setPortForSetupNode(e.target.value);
-  };
-
-  alterNodeCount = (e) => {
-    SetupActions.setNodeCount(e.target.value);
-  };
-
-  finishClusterSetup = () => {
-    SetupActions.finishClusterSetup('CouchDB Cluster set up!');
-  };
-
-  state = this.getStoreState();
-
-  render() {
-
-    return (
-      <div className="setup-nodes">
-        Setup your initial base-node, afterwards add the other nodes that you want to add
-        <div className="setup-setupnode-section">
-          <SetupCurrentAdminPassword
-            onAlterUsername={this.alterUsername}
-            onAlterPassword={this.alterPassword}
-            adminParty={this.state.isAdminParty} />
-
-          <SetupOptionalSettings
-            onAlterPort={this.alterPortSetupNode}
-            onAlterBindAddress={this.alterBindAddressSetupNode} />
-          <SetupNodeCountSetting
-            onAlterNodeCount={this.alterNodeCount} />
-        </div>
-        <hr/>
-        <div className="setup-add-nodes-section">
-          <h2>Add Nodes to the Cluster</h2>
-          <p>Remote host</p>
-          <input
-            value={this.state.remoteAddress}
-            onChange={this.alterRemoteAddressAdditionalNode}
-            className="input-remote-node"
-            type="text"
-            placeholder="IP Address" />
-
-          <SetupOptionalSettings
-            onAlterPort={this.alterPortAdditionalNode}
-            onAlterBindAddress={this.alterBindAddressAdditionalNode} />
-
-          <div className="setup-add-button">
-            <ConfirmButton
-              onClick={this.addNode}
-              showIcon={false}
-              id="setup-btn-no-thanks"
-              text="Add Node" />
-          </div>
-        </div>
-        <div className="setup-nodelist">
-          {this.getNodeList()}
-        </div>
-
-        <div className="centered setup-finish">
-          <ConfirmButton onClick={this.finishClusterSetup} showIcon={false} text="Configure Cluster" />
-        </div>
-      </div>
-    );
-  }
-}
-
-class SetupSingleNodeController extends React.Component {
-  getStoreState = () => {
-    return {
-      isAdminParty: setupStore.getIsAdminParty()
-    };
-  };
-
-  componentDidMount() {
-    setupStore.on('change', this.onChange, this);
-  }
-
-  componentWillUnmount() {
-    setupStore.off('change', this.onChange);
-  }
-
-  onChange = () => {
-    this.setState(this.getStoreState());
-  };
-
-  alterUsername = (e) => {
-    SetupActions.setUsername(e.target.value);
-  };
-
-  alterPassword = (e) => {
-    SetupActions.setPassword(e.target.value);
-  };
-
-  alterBindAddress = (e) => {
-    SetupActions.setBindAddressForSetupNode(e.target.value);
-  };
-
-  alterPort = (e) => {
-    SetupActions.setPortForSetupNode(e.target.value);
-  };
-
-  render() {
-    return (
-      <div className="setup-nodes">
-        <div className="setup-setupnode-section">
-          <SetupCurrentAdminPassword
-            onAlterUsername={this.alterUsername}
-            onAlterPassword={this.alterPassword}
-            adminParty={this.state.isAdminParty} />
-          <SetupOptionalSettings
-            onAlterPort={this.alterPort}
-            onAlterBindAddress={this.alterBindAddress} />
-          <ConfirmButton
-            onClick={this.finishSingleNode}
-            text="Configure Node" />
-        </div>
-      </div>
-    );
-  }
-
-  finishSingleNode = (e) => {
-    e.preventDefault();
-    SetupActions.setupSingleNode();
-  };
-
-  state = this.getStoreState();
-}
-
-class SetupFirstStepController extends React.Component {
-  getStoreState = () => {
-    return {
-      clusterState: setupStore.getClusterState()
-    };
-  };
-
-  componentDidMount() {
-    setupStore.on('change', this.onChange, this);
-  }
-
-  componentWillUnmount() {
-    setupStore.off('change', this.onChange);
-  }
-
-  onChange = () => {
-    this.setState(this.getStoreState());
-  };
-
-  render() {
-    if (this.state.clusterState === 'cluster_finished' ||
-        this.state.clusterState === 'single_node_enabled') {
-      return (<ClusterConfiguredScreen />);
-    }
-
-    return (
-      <div className="setup-screen">
-        <h2>Welcome to {app.i18n.en_US['couchdb-productname']}!</h2>
-        <p>
-          This wizard should be run directly on the node, rather than through a load-balancer.
-        </p>
-        <p>
-          You can configure a single node, or a multi-node CouchDB installation.
-        </p>
-        <div>
-          <ConfirmButton
-            onClick={this.redirectToMultiNodeSetup}
-            showIcon={false}
-            text="Configure a Cluster" />
-          <ConfirmButton
-            onClick={this.redirectToSingleNodeSetup}
-            showIcon={false}
-            id="setup-btn-no-thanks"
-            text="Configure a Single Node" />
-        </div>
-      </div>
-    );
-  }
-
-  redirectToSingleNodeSetup = (e) => {
-    e.preventDefault();
-    FauxtonAPI.navigate('#setup/singlenode');
-  };
-
-  redirectToMultiNodeSetup = (e) => {
-    e.preventDefault();
-    FauxtonAPI.navigate('#setup/multinode');
-  };
-
-  state = this.getStoreState();
-}
-
-export default {
-  SetupMultipleNodesController: SetupMultipleNodesController,
-  SetupFirstStepController: SetupFirstStepController,
-  ClusterConfiguredScreen: ClusterConfiguredScreen,
-  SetupSingleNodeController: SetupSingleNodeController,
-  SetupOptionalSettings: SetupOptionalSettings
-};
diff --git a/app/addons/setup/setup.stores.js b/app/addons/setup/setup.stores.js
deleted file mode 100644
index 2092ab9..0000000
--- a/app/addons/setup/setup.stores.js
+++ /dev/null
@@ -1,198 +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 "./setup.actiontypes";
-
-var SetupStore = FauxtonAPI.Store.extend({
-
-  initialize: function () {
-    this.reset();
-  },
-
-  reset: function () {
-    this._clusterState = [];
-
-    this._username = '';
-    this._password = '';
-
-    this._setupNode = {
-      bindAddress: '0.0.0.0',
-      port: 5984,
-      nodeCount: 3
-    };
-
-    this.resetAddtionalNode();
-
-    this._nodeList = [];
-  },
-
-  resetAddtionalNode: function () {
-    this._additionalNode = {
-      bindAddress: '0.0.0.0',
-      port: 5984,
-      remoteAddress: '127.0.0.1'
-    };
-  },
-
-  setClusterState: function (options) {
-    this._clusterState = options.state;
-  },
-
-  getClusterState: function () {
-    return this._clusterState;
-  },
-
-  getNodeList: function () {
-    return this._nodeList;
-  },
-
-  getIsAdminParty: function () {
-    return FauxtonAPI.session.isAdminParty();
-  },
-
-  setUsername: function (options) {
-    this._username = options.value;
-  },
-
-  setPassword: function (options) {
-    this._password = options.value;
-  },
-
-  getUsername: function () {
-    return this._username;
-  },
-
-  getPassword: function () {
-    return this._password;
-  },
-
-  setBindAdressForSetupNode: function (options) {
-    this._setupNode.bindAddress = options.value;
-  },
-
-  setPortForSetupNode: function (options) {
-    this._setupNode.port = options.value;
-  },
-
-  getPortForSetupNode: function () {
-    return this._setupNode.port;
-  },
-
-  getBindAdressForSetupNode: function () {
-    return this._setupNode.bindAddress;
-  },
-
-  setNodeCountForSetupNode: function (options) {
-    this._setupNode.nodeCount = Math.min(options.value, 3);
-  },
-
-  getNodeCountForSetupNode: function () {
-    return this._setupNode.nodeCount;
-  },
-
-  setBindAdressForAdditionalNode: function (options) {
-    this._additionalNode.bindAddress = options.value;
-  },
-
-  setPortForAdditionalNode: function (options) {
-    this._additionalNode.port = options.value;
-  },
-
-  setRemoteAddressForAdditionalNode: function (options) {
-    this._additionalNode.remoteAddress = options.value;
-  },
-
-  getAdditionalNode: function () {
-    return this._additionalNode;
-  },
-
-  addNodeToList: function (options) {
-    this._nodeList.push(options.value);
-    this.resetAddtionalNode();
-  },
-
-  getHostForSetupNode: function () {
-    return '127.0.0.1';
-  },
-
-  dispatch: function (action) {
-
-    switch (action.type) {
-      case ActionTypes.SETUP_SET_CLUSTERSTATUS:
-        this.setClusterState(action.options);
-        break;
-
-      case ActionTypes.SETUP_SET_USERNAME:
-        this.setUsername(action.options);
-        break;
-
-      case ActionTypes.SETUP_SET_PASSWORD:
-        this.setPassword(action.options);
-        break;
-
-      case ActionTypes.SETUP_BIND_ADDRESS_FOR_SINGLE_NODE:
-        this.setBindAdressForSetupNode(action.options);
-        break;
-
-      case ActionTypes.SETUP_PORT_FOR_SINGLE_NODE:
-        this.setPortForSetupNode(action.options);
-        break;
-
-      case ActionTypes.SETUP_PORT_ADDITIONAL_NODE:
-        this.setPortForAdditionalNode(action.options);
-        break;
-
-      case ActionTypes.SETUP_BIND_ADDRESS_ADDITIONAL_NODE:
-        this.setBindAdressForAdditionalNode(action.options);
-        break;
-
-      case ActionTypes.SETUP_REMOTE_ADDRESS_ADDITIONAL_NODE:
-        this.setRemoteAddressForAdditionalNode(action.options);
-        break;
-
-      case ActionTypes.SETUP_ADD_NODE_TO_LIST:
-        this.addNodeToList(action.options);
-        break;
-
-      case ActionTypes.SETUP_RESET_ADDITIONAL_NODE:
-        this.resetAddtionalNode();
-        break;
-
-      case ActionTypes.SETUP_NODE_COUNT:
-        this.setNodeCountForSetupNode(action.options);
-        break;
-
-      default:
-        return;
-    }
-
-    //This is a quick and somewhat messy fix
-    //Some of the way our components are linked together can cause a component to be re-rendered
-    //even after it is unmounted.
-    // This fix stops that from happening
-    setTimeout(() => {
-      this.triggerChange();
-    });
-  }
-
-});
-
-
-var setupStore = new SetupStore();
-
-setupStore.dispatchToken = FauxtonAPI.dispatcher.register(setupStore.dispatch.bind(setupStore));
-
-export default {
-  setupStore: setupStore,
-  SetupStore: SetupStore
-};
diff --git a/app/constants.js b/app/constants.js
index ecb64e3..5fd260b 100644
--- a/app/constants.js
+++ b/app/constants.js
@@ -46,6 +46,7 @@ export default {
     VIEWS: '/_utils/docs/intro/overview.html#views',
     MANGO_INDEX: '/_utils/docs/intro/api.html#documents',
     MANGO_SEARCH: '/_utils/docs/intro/api.html#documents',
+    SETUP: '/_utils/docs/cluster/setup.html#the-cluster-setup-wizard',
     CHANGES: '/_utils/docs/api/database/changes.html?highlight=changes#post--db-_changes'
   }
 };