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>
diff --git a/.gitignore b/.gitignore
index 6a06e9c..988ac27 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,7 @@
!app/addons/documents
!app/addons/styletests
!app/addons/cors
+!app/addons/setup
settings.json*
i18n.json
!settings.json.default
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 @@
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>
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 @@
);
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);
+ });
});
});
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 @@
function (app, FauxtonAPI, Config) {
Config.initialize = function () {
FauxtonAPI.addHeaderLink({
- title: 'Config',
+ title: 'Configuration',
href: '#_config',
icon: 'fonticon-cog',
className: 'config'
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;
+ }
+
+}
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;
+});
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;
+});
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;
+});
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
+ }
+ });
+ }
+ };
+ });
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',
+ };
+});
+
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
+ };
+});
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
+ };
+});
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');
+ });
+
+ });
+
+ });
+
+});
+
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);
+ });
+
+ });
+});
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"
}
}
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" },