couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From robertkowal...@apache.org
Subject fauxton commit: updated refs/heads/master to e6a3514
Date Fri, 30 Oct 2015 11:13:55 GMT
Repository: couchdb-fauxton
Updated Branches:
  refs/heads/master 5fcb06539 -> e6a351437


add a wizard for setting up a cluster

how to test:

set the port in `exports.couch` to a node that will handle the
setup (the "setup-node"):

```
exports.couch = 'http://localhost:15984/';
```

if you change the port of the setup node during setup the wizard will
lose the connection and can't finish.

PR: #529
PR-URL: https://github.com/apache/couchdb-fauxton/pull/529
Reviewed-By: Michelle Phung <michellep@apache.org>


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

Branch: refs/heads/master
Commit: e6a35143709fbba4a0bd760fed4ccbc49b79f34a
Parents: 5fcb065
Author: Robert Kowalski <robertkowalski@apache.org>
Authored: Mon Jun 29 19:05:24 2015 +0200
Committer: Robert Kowalski <robertkowalski@apache.org>
Committed: Fri Oct 30 12:13:17 2015 +0100

----------------------------------------------------------------------
 .gitignore                                      |   1 +
 .../components/react-components.react.jsx       |   2 +-
 .../tests/confirmButtonSpec.react.jsx           |  12 +
 app/addons/config/base.js                       |   2 +-
 app/addons/setup/assets/less/setup.less         |  63 +++
 app/addons/setup/base.js                        |  29 ++
 app/addons/setup/resources.js                   |  52 +++
 app/addons/setup/route.js                       |  72 ++++
 app/addons/setup/setup.actions.js               | 285 ++++++++++++++
 app/addons/setup/setup.actiontypes.js           |  27 ++
 app/addons/setup/setup.react.jsx                | 383 +++++++++++++++++++
 app/addons/setup/setup.stores.js                | 184 +++++++++
 .../setup/tests/setupComponentsSpec.react.jsx   | 145 +++++++
 app/addons/setup/tests/setupSpec.js             |  74 ++++
 i18n.json.default                               |   3 +-
 settings.json.default                           |   1 +
 16 files changed, 1332 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index 6a06e9c..988ac27 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,7 @@ app/addons/*
 !app/addons/documents
 !app/addons/styletests
 !app/addons/cors
+!app/addons/setup
 settings.json*
 i18n.json
 !settings.json.default

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/app/addons/components/react-components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/components/react-components.react.jsx b/app/addons/components/react-components.react.jsx
index 8e62a9e..7154070 100644
--- a/app/addons/components/react-components.react.jsx
+++ b/app/addons/components/react-components.react.jsx
@@ -1021,7 +1021,7 @@ function (app, FauxtonAPI, React, Stores, FauxtonComponents, ace, beautifyHelper
   var ConfirmButton = React.createClass({
     render: function () {
       return (
-        <button type="submit" className="btn btn-success save" id={this.props.id}>
+        <button onClick={this.props.onClick} type="submit" className="btn btn-success
save" id={this.props.id}>
           <i className="icon fonticon-ok-circled"></i>
           {this.props.text}
         </button>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/app/addons/components/tests/confirmButtonSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/components/tests/confirmButtonSpec.react.jsx b/app/addons/components/tests/confirmButtonSpec.react.jsx
index d6d1a9c..d4d428a 100644
--- a/app/addons/components/tests/confirmButtonSpec.react.jsx
+++ b/app/addons/components/tests/confirmButtonSpec.react.jsx
@@ -37,5 +37,17 @@ define([
       );
       assert.equal($(button.getDOMNode()).text(), 'Click here to render Rocko Artischocko');
     });
+
+    it('should use onClick handler if provided', function () {
+      var spy = sinon.spy();
+
+      button = TestUtils.renderIntoDocument(
+        <ReactComponents.ConfirmButton text="Click here" onClick={spy} />,
+        container
+      );
+
+      React.addons.TestUtils.Simulate.click(button.getDOMNode());
+      assert.ok(spy.calledOnce);
+    });
   });
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/app/addons/config/base.js
----------------------------------------------------------------------
diff --git a/app/addons/config/base.js b/app/addons/config/base.js
index 229c48f..95a2cf0 100644
--- a/app/addons/config/base.js
+++ b/app/addons/config/base.js
@@ -22,7 +22,7 @@ define([
 function (app, FauxtonAPI, Config) {
   Config.initialize = function () {
     FauxtonAPI.addHeaderLink({
-      title: 'Config',
+      title: 'Configuration',
       href: '#_config',
       icon: 'fonticon-cog',
       className: 'config'

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/app/addons/setup/assets/less/setup.less
----------------------------------------------------------------------
diff --git a/app/addons/setup/assets/less/setup.less b/app/addons/setup/assets/less/setup.less
new file mode 100644
index 0000000..577699d
--- /dev/null
+++ b/app/addons/setup/assets/less/setup.less
@@ -0,0 +1,63 @@
+// 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.
+
+.setup-screen {
+  padding: 20px;
+  button {
+    margin-top: 20px;
+  }
+  #setup-btn-no-thanks {
+    margin-left: 30px;
+  }
+}
+
+.setup-nodes {
+  input {
+    margin-right: 15px;
+  }
+
+  h2 {
+    font-size: 16px;
+    line-height: normal;
+    margin: 0;
+    text-transform: uppercase;
+  }
+
+  .node-item {
+    width: 400px;
+    a {
+      margin-left: 10px;
+    }
+  }
+
+  .input-remote-node {
+    width: 50%;
+  }
+
+  .centered {
+    text-align: center;
+  }
+
+  .setup-finish,
+  .setup-nodelist,
+  .setup-opt-settings,
+  .setup-creds,
+  .setup-port,
+  .setup-add-button {
+    margin-top: 30px;
+  }
+
+  .setup-finish {
+    padding-bottom: 40px;
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/app/addons/setup/base.js
----------------------------------------------------------------------
diff --git a/app/addons/setup/base.js b/app/addons/setup/base.js
new file mode 100644
index 0000000..51b716c
--- /dev/null
+++ b/app/addons/setup/base.js
@@ -0,0 +1,29 @@
+// 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.
+
+define([
+  'app',
+  'api',
+  'addons/setup/route'
+],
+
+function (app, FauxtonAPI, Setup) {
+  Setup.initialize = function () {
+    FauxtonAPI.addHeaderLink({
+      title: 'Setup',
+      href: "#setup",
+      icon: 'fonticon-wrench'
+    });
+  };
+
+  return Setup;
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/app/addons/setup/resources.js
----------------------------------------------------------------------
diff --git a/app/addons/setup/resources.js b/app/addons/setup/resources.js
new file mode 100644
index 0000000..f5aab33
--- /dev/null
+++ b/app/addons/setup/resources.js
@@ -0,0 +1,52 @@
+// 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.
+
+define([
+  'app',
+  'api'
+],
+
+function (app, FauxtonAPI) {
+
+  var Setup = FauxtonAPI.addon();
+
+
+  Setup.Model = Backbone.Model.extend({
+
+    documentation: app.host + '/_utils/docs',
+
+    url: function () {
+      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') {
+        return 'Bind address can not be 127.0.0.1';
+      }
+
+      if (attrs.port && _.isNaN(+attrs.port)) {
+        return 'Bind port must be a number';
+      }
+    }
+
+  });
+
+  return Setup;
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/app/addons/setup/route.js
----------------------------------------------------------------------
diff --git a/app/addons/setup/route.js b/app/addons/setup/route.js
new file mode 100644
index 0000000..c3bbe39
--- /dev/null
+++ b/app/addons/setup/route.js
@@ -0,0 +1,72 @@
+// 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.
+
+define([
+  'app',
+  'api',
+  'addons/setup/resources',
+  'addons/setup/setup.react',
+  'addons/setup/setup.actions',
+  'addons/cluster/cluster.actions',
+
+],
+function (app, FauxtonAPI, Setup, SetupComponents, SetupActions, ClusterActions) {
+  var RouteObject = FauxtonAPI.RouteObject.extend({
+    layout: 'one_pane',
+
+    roles: ['_admin'],
+
+    routes: {
+      'setup': 'setupInitView',
+      'setup/finish': 'finishView',
+      'setup/singlenode': 'setupSingleNode',
+      'setup/multinode': 'setupMultiNode'
+    },
+
+    crumbs: [
+      {'name': 'Setup ' + app.i18n.en_US['couchdb-productname'], 'link': 'setup'}
+    ],
+
+    apiUrl: function () {
+      return [this.setupModel.url(), this.setupModel.documentation];
+    },
+
+    initialize: function () {
+      this.setupModel = new Setup.Model();
+    },
+
+    setupInitView: function () {
+      ClusterActions.fetchNodes();
+      SetupActions.getClusterStateFromCouch();
+      this.setComponent('#dashboard-content', SetupComponents.SetupFirstStepController);
+    },
+
+    setupSingleNode: function () {
+      ClusterActions.fetchNodes();
+      this.setComponent('#dashboard-content', SetupComponents.SetupSingleNodeController);
+    },
+
+    setupMultiNode: function () {
+      ClusterActions.fetchNodes();
+      this.setComponent('#dashboard-content', SetupComponents.SetupMultipleNodesController);
+    },
+
+    finishView: function () {
+      this.setComponent('#dashboard-content', SetupComponents.ClusterConfiguredScreen);
+    }
+  });
+
+
+  Setup.RouteObjects = [RouteObject];
+
+  return Setup;
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/app/addons/setup/setup.actions.js
----------------------------------------------------------------------
diff --git a/app/addons/setup/setup.actions.js b/app/addons/setup/setup.actions.js
new file mode 100644
index 0000000..c1519c8
--- /dev/null
+++ b/app/addons/setup/setup.actions.js
@@ -0,0 +1,285 @@
+// 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.
+define([
+  'api',
+  'addons/config/resources',
+  'addons/setup/resources',
+  'addons/setup/setup.actiontypes',
+  'addons/cluster/cluster.stores',
+  'addons/setup/setup.stores',
+
+], function (FauxtonAPI, ConfigResources, SetupResources, ActionTypes, ClusterStores, SetupStores)
{
+    var nodesStore = ClusterStores.nodesStore;
+    var setupStore = SetupStores.setupStore;
+
+    return {
+
+      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 (res) {
+          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 nodes = nodesStore.getNodes();
+        var isAdminParty = setupStore.getIsAdminParty();
+        var username = setupStore.getUsername();
+        var password = setupStore.getPassword();
+
+        var setupModel = new SetupResources.Model({
+          action: 'enable_cluster',
+          username: username,
+          password: password,
+          bind_address: setupStore.getBindAdressForSetupNode(),
+          port: setupStore.getPortForSetupNode()
+        });
+
+        setupModel.on('invalid', function (model, error) {
+          FauxtonAPI.addNotification({
+            msg: error,
+            type: 'error',
+            fade: false,
+            clear: true
+          });
+        });
+
+        setupModel.save()
+          .then(function () {
+            return FauxtonAPI.session.login(username, password);
+          })
+          .then(function () {
+            return this.finishClusterSetup('CouchDB is set up!');
+          }.bind(this));
+      },
+
+      addNode: function (isOrWasAdminParty) {
+        var username = setupStore.getUsername();
+        var password = setupStore.getPassword();
+        var portForSetupNode = setupStore.getPortForSetupNode();
+        var bindAddressForSetupNode = setupStore.getBindAdressForSetupNode();
+
+        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
+        });
+
+        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,
+          remote_node: remoteAddressForAdditionalNode,
+          remote_current_user: username,
+          remote_current_password: password
+        };
+
+        if (isOrWasAdminParty) {
+          delete additionalNodeData.remote_current_user;
+          delete additionalNodeData.remote_current_password;
+        }
+
+        function dontGiveUp (f, u, p) {
+          return f(u, p).then(
+            undefined,
+            function (err) {
+              return dontGiveUp(f, u, p);
+            }
+          );
+        }
+
+        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 () {
+            FauxtonAPI.session.login(username, password).then(function () {
+              continueSetup();
+            });
+          });
+
+        function continueSetup () {
+          var addNodeModel = new SetupResources.Model({
+            action: 'add_node',
+            username: username,
+            password: password,
+            host: remoteAddressForAdditionalNode,
+            port: portForForAdditionalNode
+          });
+
+          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
+          }
+        });
+      }
+    };
+  });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/app/addons/setup/setup.actiontypes.js
----------------------------------------------------------------------
diff --git a/app/addons/setup/setup.actiontypes.js b/app/addons/setup/setup.actiontypes.js
new file mode 100644
index 0000000..6bbd390
--- /dev/null
+++ b/app/addons/setup/setup.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.
+
+define([], function () {
+  return {
+    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',
+  };
+});
+

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/app/addons/setup/setup.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/setup/setup.react.jsx b/app/addons/setup/setup.react.jsx
new file mode 100644
index 0000000..647f09b
--- /dev/null
+++ b/app/addons/setup/setup.react.jsx
@@ -0,0 +1,383 @@
+// 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.
+
+define([
+  'app',
+  'api',
+  'react',
+  'addons/components/react-components.react',
+  'addons/setup/setup.actions',
+  'addons/setup/setup.stores',
+
+
+], function (app, FauxtonAPI, React, ReactComponents, SetupActions, SetupStores) {
+
+  var setupStore = SetupStores.setupStore;
+  var ConfirmButton = ReactComponents.ConfirmButton;
+
+
+  var ClusterConfiguredScreen = React.createClass({
+
+    render: function () {
+      return (
+        <div className="setup-screen">
+          {app.i18n.en_US['couchdb-productname']} is configured for production usage!
+          <br />
+          <br/>
+          Do you want to <a href="#replication">replicate data</a>?
+        </div>
+      );
+    }
+  });
+
+  var SetupCurrentAdminPassword = React.createClass({
+
+    render: function () {
+      var text = 'Your current Admin Username & Password';
+
+      if (this.props.adminParty) {
+        text = 'Admin Username & Password that you want to use';
+      }
+
+      return (
+        <div className="setup-creds">
+          <div>
+            <h2>Specify Credentials</h2>
+            {text}
+          </div>
+          <input
+            className="setup-username"
+            onChange={this.props.onAlterUsername}
+            placeholder="Admin Username"
+            type="text" />
+          <input
+            className="setup-password"
+            onChange={this.props.onAlterPassword}
+            placeholder="Admin Password"
+            type="password" />
+        </div>
+      );
+    },
+
+
+  });
+
+
+  var SetupOptionalSettings = React.createClass({
+    getInitialState: function () {
+      return {
+        ipValue: this.props.ipInitialValue,
+        portValue: this.props.portValue
+      };
+    },
+
+    handleIpChange: function (event) {
+      this.props.onAlterBindAddress(event);
+      this.setState({ipValue: event.target.value});
+    },
+
+    handlePortChange: function (event) {
+      this.props.onAlterPort(event);
+      this.setState({portValue: event.target.value});
+    },
+
+    render: function () {
+      return (
+        <div className="setup-opt-settings">
+          <h2>IP</h2>
+          Bind address to listen on<br/>
+
+          <input
+            className="setup-input-ip"
+            value={this.state.ipValue}
+            onChange={this.handleIpChange}
+            defaultValue="0.0.0.0"
+            type="text" />
+
+          <div className="setup-port">
+            <h2>Port</h2>
+            Port that the Node uses <br/>
+            <input
+              className="setup-input-port"
+              value={this.state.portValue}
+              onChange={this.handlePortChange}
+              defaultValue="5984"
+              type="text" />
+          </div>
+        </div>
+      );
+    }
+  });
+
+  var SetupMultipleNodesController = React.createClass({
+
+    getInitialState: function () {
+      return this.getStoreState();
+    },
+
+    getStoreState: function () {
+      return {
+        nodeList: setupStore.getNodeList(),
+        isAdminParty: setupStore.getIsAdminParty(),
+        remoteAddress: setupStore.getAdditionalNode().remoteAddress
+      };
+    },
+
+    componentDidMount: function () {
+      this.isAdminParty = setupStore.getIsAdminParty();
+      setupStore.on('change', this.onChange, this);
+    },
+
+    componentWillUnmount: function () {
+      setupStore.off('change', this.onChange);
+    },
+
+    onChange: function () {
+      if (this.isMounted()) {
+        this.setState(this.getStoreState());
+      }
+    },
+
+    getNodeList: function () {
+      return this.state.nodeList.map(function (el, i) {
+        return (
+          <div key={i} className="node-item">
+            {el.remoteAddress}:{el.port}
+          </div>
+        );
+      }, this);
+    },
+
+    addNode: function () {
+      SetupActions.addNode(this.isAdminParty);
+    },
+
+    alterPortAdditionalNode: function (e) {
+      SetupActions.alterPortAdditionalNode(e.target.value);
+    },
+
+    alterBindAddressAdditionalNode: function (e) {
+      SetupActions.alterBindAddressAdditionalNode(e.target.value);
+    },
+
+    alterRemoteAddressAdditionalNode: function (e) {
+      SetupActions.alterRemoteAddressAdditionalNode(e.target.value);
+    },
+
+    alterUsername: function (e) {
+      SetupActions.setUsername(e.target.value);
+    },
+
+    alterPassword: function (e) {
+      SetupActions.setPassword(e.target.value);
+    },
+
+    alterBindAddressSetupNode: function (e) {
+      SetupActions.setBindAddressForSetupNode(e.target.value);
+    },
+
+    alterPortSetupNode: function (e) {
+      SetupActions.setPortForSetupNode(e.target.value);
+    },
+
+    finishClusterSetup: function () {
+      SetupActions.finishClusterSetup('CouchDB Cluster set up!');
+    },
+
+    render: function () {
+
+      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} />
+            </div>
+          <hr/>
+          <div className="setup-add-nodes-section">
+            <h2>Add Nodes</h2>
+            Remote host <br/>
+            <input
+              value={this.state.remoteAddress}
+              onChange={this.alterRemoteAddressAdditionalNode}
+              className="input-remote-node"
+              type="text"
+              placeholder="127.0.0.1" />
+
+            <SetupOptionalSettings
+              onAlterPort={this.alterPortAdditionalNode}
+              onAlterBindAddress={this.alterBindAddressAdditionalNode} />
+
+            <div className="setup-add-button">
+              <ConfirmButton
+                onClick={this.addNode}
+                id="setup-btn-no-thanks"
+                text="ADD" />
+            </div>
+          </div>
+          <div className="setup-nodelist">
+            {this.getNodeList()}
+          </div>
+
+          <div className="centered setup-finish">
+            <ConfirmButton onClick={this.finishClusterSetup} text="SETUP" />
+          </div>
+        </div>
+      );
+    }
+  });
+
+  var SetupSingleNodeController = React.createClass({
+
+    getInitialState: function () {
+      return this.getStoreState();
+    },
+
+    getStoreState: function () {
+      return {
+        isAdminParty: setupStore.getIsAdminParty()
+      };
+    },
+
+    componentDidMount: function () {
+      setupStore.on('change', this.onChange, this);
+    },
+
+    componentWillUnmount: function () {
+      setupStore.off('change', this.onChange);
+    },
+
+    onChange: function () {
+      if (this.isMounted()) {
+        this.setState(this.getStoreState());
+      }
+    },
+
+    alterUsername: function (e) {
+      SetupActions.setUsername(e.target.value);
+    },
+
+    alterPassword: function (e) {
+      SetupActions.setPassword(e.target.value);
+    },
+
+    alterBindAddress: function (e) {
+      SetupActions.setBindAddressForSetupNode(e.target.value);
+    },
+
+    alterPort: function (e) {
+      SetupActions.setPortForSetupNode(e.target.value);
+    },
+
+    render: function () {
+      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="Finish" />
+          </div>
+        </div>
+      );
+    },
+
+    finishSingleNode: function (e) {
+      e.preventDefault();
+      SetupActions.setupSingleNode();
+    }
+  });
+
+  var SetupFirstStepController = React.createClass({
+
+    getInitialState: function () {
+      return this.getStoreState();
+    },
+
+    getStoreState: function () {
+      return {
+        clusterState: setupStore.getClusterState()
+      };
+    },
+
+    componentDidMount: function () {
+      setupStore.on('change', this.onChange, this);
+    },
+
+    componentWillUnmount: function () {
+      setupStore.off('change', this.onChange);
+    },
+
+    onChange: function () {
+      if (this.isMounted()) {
+        this.setState(this.getStoreState());
+      }
+    },
+
+    render: function () {
+      if (this.state.clusterState === 'cluster_finished') {
+        return (<ClusterConfiguredScreen />);
+      }
+
+      return (
+        <div className="setup-screen">
+          <h2>Welcome to {app.i18n.en_US['couchdb-productname']}!</h2>
+          <p>
+            The recommended way to run the wizard is directly on your
+            node (e.g without a Loadbalancer) in front of it.
+          </p>
+          <p>
+            Do you want to setup a cluster with multiple nodes
+            or just a single node CouchDB installation?
+          </p>
+          <div>
+            <ConfirmButton
+              onClick={this.redirectToMultiNodeSetup}
+              text="Setup cluster" />
+            <ConfirmButton
+              onClick={this.redirectToSingleNodeSetup}
+              id="setup-btn-no-thanks"
+              text="Single-Node-Setup" />
+          </div>
+        </div>
+      );
+    },
+
+    redirectToSingleNodeSetup: function (e) {
+      e.preventDefault();
+      FauxtonAPI.navigate('#setup/singlenode');
+    },
+
+    redirectToMultiNodeSetup: function (e) {
+      e.preventDefault();
+      FauxtonAPI.navigate('#setup/multinode');
+    }
+  });
+
+  return {
+    SetupMultipleNodesController: SetupMultipleNodesController,
+    SetupFirstStepController: SetupFirstStepController,
+    ClusterConfiguredScreen: ClusterConfiguredScreen,
+    SetupSingleNodeController: SetupSingleNodeController,
+    SetupOptionalSettings: SetupOptionalSettings
+  };
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/app/addons/setup/setup.stores.js
----------------------------------------------------------------------
diff --git a/app/addons/setup/setup.stores.js b/app/addons/setup/setup.stores.js
new file mode 100644
index 0000000..58c4bb1
--- /dev/null
+++ b/app/addons/setup/setup.stores.js
@@ -0,0 +1,184 @@
+// 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.
+
+define([
+  'api',
+  'addons/setup/setup.actiontypes'
+
+], function (FauxtonAPI, 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
+      };
+
+      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;
+    },
+
+    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;
+
+
+        default:
+        return;
+      }
+
+      this.triggerChange();
+    }
+
+  });
+
+
+  var setupStore = new SetupStore();
+
+  setupStore.dispatchToken = FauxtonAPI.dispatcher.register(setupStore.dispatch.bind(setupStore));
+
+  return {
+    setupStore: setupStore,
+    SetupStore: SetupStore
+  };
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/app/addons/setup/tests/setupComponentsSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/setup/tests/setupComponentsSpec.react.jsx b/app/addons/setup/tests/setupComponentsSpec.react.jsx
new file mode 100644
index 0000000..ead1f98
--- /dev/null
+++ b/app/addons/setup/tests/setupComponentsSpec.react.jsx
@@ -0,0 +1,145 @@
+// 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.
+define([
+  'api',
+  'addons/setup/setup.react',
+  'addons/setup/setup.stores',
+  'testUtils',
+  'react'
+], function (FauxtonAPI, Views, Stores, utils, React) {
+
+  var assert = utils.assert;
+  var TestUtils = React.addons.TestUtils;
+
+  describe('Setup Components', function () {
+
+    describe('IP / Port area', function () {
+      var changeHandler, container;
+
+      beforeEach(function () {
+        changeHandler = sinon.spy();
+        container = document.createElement('div');
+      });
+
+      afterEach(function () {
+        React.unmountComponentAtNode(container);
+      });
+
+      it('fires callbacks on change, ip', function () {
+        var optSettings = TestUtils.renderIntoDocument(
+          <Views.SetupOptionalSettings onAlterPort={null} onAlterBindAddress={changeHandler}
/>,
+          container
+        );
+
+        var node = $(optSettings.getDOMNode()).find('.setup-input-ip')[0];
+        TestUtils.Simulate.change(node, {target: {value: 'Hello, world'}});
+
+        assert.ok(changeHandler.calledOnce);
+      });
+
+      it('fires callbacks on change, port', function () {
+        var optSettings = TestUtils.renderIntoDocument(
+          <Views.SetupOptionalSettings onAlterPort={changeHandler} onAlterBindAddress={null}
/>,
+          container
+        );
+
+        var node = $(optSettings.getDOMNode()).find('.setup-input-port')[0];
+        TestUtils.Simulate.change(node, {target: {value: 'Hello, world'}});
+
+        assert.ok(changeHandler.calledOnce);
+      });
+
+    });
+
+    describe('SetupMultipleNodesController', function () {
+      var controller, changeHandler, container;
+
+      beforeEach(function () {
+        sinon.stub(Stores.setupStore, 'getIsAdminParty', function () { return false; });
+        container = document.createElement('div');
+        controller = TestUtils.renderIntoDocument(
+          <Views.SetupMultipleNodesController />,
+          container
+        );
+      });
+
+      afterEach(function () {
+        utils.restore(Stores.setupStore.getIsAdminParty);
+        React.unmountComponentAtNode(container);
+        Stores.setupStore.reset();
+      });
+
+      it('changes the values in the store for additional nodes', function () {
+        var $addNodesSection = $(controller.getDOMNode()).find('.setup-add-nodes-section');
+        TestUtils.Simulate.change($addNodesSection.find('.setup-input-ip')[0], {target: {value:
'192.168.13.37'}});
+        TestUtils.Simulate.change($addNodesSection.find('.setup-input-port')[0], {target:
{value: '1337'}});
+        TestUtils.Simulate.change($addNodesSection.find('.input-remote-node')[0], {target:
{value: 'node2.local'}});
+
+        var additionalNode = Stores.setupStore.getAdditionalNode();
+        assert.equal(additionalNode.bindAddress, '192.168.13.37');
+        assert.equal(additionalNode.remoteAddress, 'node2.local');
+        assert.equal(additionalNode.port, '1337');
+      });
+
+      it('changes the values in the store for the setup node', function () {
+        var $setupNodesSection = $(controller.getDOMNode()).find('.setup-setupnode-section');
+        TestUtils.Simulate.change($setupNodesSection.find('.setup-input-ip')[0], {target:
{value: '192.168.42.42'}});
+        TestUtils.Simulate.change($setupNodesSection.find('.setup-input-port')[0], {target:
{value: '4242'}});
+        TestUtils.Simulate.change($setupNodesSection.find('.setup-username')[0], {target:
{value: 'tester'}});
+        TestUtils.Simulate.change($setupNodesSection.find('.setup-password')[0], {target:
{value: 'testerpass'}});
+
+
+        assert.equal(Stores.setupStore.getBindAdressForSetupNode(), '192.168.42.42');
+        assert.equal(Stores.setupStore.getPortForSetupNode(), '4242');
+        assert.equal(Stores.setupStore.getUsername(), 'tester');
+        assert.equal(Stores.setupStore.getPassword(), 'testerpass');
+      });
+
+    });
+
+    describe('SingleNodeSetup', function () {
+      var controller, changeHandler, container;
+
+      beforeEach(function () {
+        sinon.stub(Stores.setupStore, 'getIsAdminParty', function () { return false; });
+        container = document.createElement('div');
+        controller = TestUtils.renderIntoDocument(
+          <Views.SetupSingleNodeController />,
+          container
+        );
+      });
+
+      afterEach(function () {
+        utils.restore(Stores.setupStore.getIsAdminParty);
+        React.unmountComponentAtNode(container);
+        Stores.setupStore.reset();
+      });
+
+      it('changes the values in the store for the setup node', function () {
+        var $setupNodesSection = $(controller.getDOMNode()).find('.setup-setupnode-section');
+        TestUtils.Simulate.change($setupNodesSection.find('.setup-input-ip')[0], {target:
{value: '192.168.13.42'}});
+        TestUtils.Simulate.change($setupNodesSection.find('.setup-input-port')[0], {target:
{value: '1342'}});
+        TestUtils.Simulate.change($setupNodesSection.find('.setup-username')[0], {target:
{value: 'tester'}});
+        TestUtils.Simulate.change($setupNodesSection.find('.setup-password')[0], {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');
+      });
+
+    });
+
+  });
+
+});
+

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/app/addons/setup/tests/setupSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/setup/tests/setupSpec.js b/app/addons/setup/tests/setupSpec.js
new file mode 100644
index 0000000..b3305a1
--- /dev/null
+++ b/app/addons/setup/tests/setupSpec.js
@@ -0,0 +1,74 @@
+// 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.
+define([
+      'api',
+      'addons/setup/resources',
+      'testUtils'
+], function (FauxtonAPI, Resources, testUtils) {
+  var assert = testUtils.assert,
+      ViewSandbox = testUtils.ViewSandbox,
+      model;
+
+  describe('Setup: verify input', function () {
+
+    beforeEach(function () {
+      model = new Resources.Model();
+    });
+
+    it('You have to set a username', function () {
+      var error = model.validate({
+        admin: {
+          user: '',
+          password: 'ente'
+        }
+      });
+
+      assert.ok(error);
+    });
+
+    it('You have to set a password', function () {
+      var error = model.validate({
+        admin: {
+          user: 'rocko',
+          password: ''
+        }
+      });
+
+      assert.ok(error);
+    });
+
+    it('Port must be a number, if defined', function () {
+      var error = model.validate({
+        admin: {
+          user: 'rocko',
+          password: 'ente'
+        },
+        port: 'port'
+      });
+
+      assert.ok(error);
+    });
+
+    it('Bind address can not be 127.0.0.1', function () {
+      var error = model.validate({
+        admin: {
+          user: 'rocko',
+          password: 'ente'
+        },
+        bind_address: '127.0.0.1'
+      });
+
+      assert.ok(error);
+    });
+
+  });
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/i18n.json.default
----------------------------------------------------------------------
diff --git a/i18n.json.default b/i18n.json.default
index 7e478dc..13f08c9 100644
--- a/i18n.json.default
+++ b/i18n.json.default
@@ -7,6 +7,7 @@
     "mango-title-editor": "Mango Query",
     "mango-descripton-index-editor": "Mango is an easy way to find documents on predefined
indexes. <br/><br/>Create an Index to query it afterwards. The example in the
editor shows how to create an index for the field '_id'. <br/><br/>The Indexes
that you already created are listed on the right.",
     "mango-additional-indexes-heading": "Your additional Indexes:",
-    "mango-indexeditor-title": "Mango"
+    "mango-indexeditor-title": "Mango",
+    "couchdb-productname": "Apache CouchDB"
   }
 }

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/e6a35143/settings.json.default
----------------------------------------------------------------------
diff --git a/settings.json.default b/settings.json.default
index 9d384ad..aae7608 100644
--- a/settings.json.default
+++ b/settings.json.default
@@ -4,6 +4,7 @@
   { "name": "components" },
   { "name": "databases" },
   { "name": "documents" },
+  { "name": "setup" },
   { "name": "activetasks" },
   { "name": "cluster" },
   { "name": "config" },


Mime
View raw message