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 <[email protected]> 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 <[email protected]> Authored: Mon Jun 29 19:05:24 2015 +0200 Committer: Robert Kowalski <[email protected]> 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" },
