This is an automated email from the ASF dual-hosted git repository. amaranhao pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/couchdb-fauxton.git
The following commit(s) were added to refs/heads/master by this push: new 09d6aca Update config addon to use redux (#1138) 09d6aca is described below commit 09d6acadace67197a428d35a223c7ba0085cfcbe Author: Antonio Maranhao <30349380+antonio-maran...@users.noreply.github.com> AuthorDate: Mon Oct 15 14:38:34 2018 -0400 Update config addon to use redux (#1138) * Split components into separate files * Use config addon to use redux * Update tests --- app/addons/config/__tests__/actions.test.js | 227 ++++------- app/addons/config/__tests__/components.test.js | 166 ++++---- app/addons/config/__tests__/reducers.test.js | 114 ++++++ app/addons/config/__tests__/stores.test.js | 94 ----- app/addons/config/actions.js | 198 +++++----- app/addons/config/api.js | 54 +++ app/addons/config/base.js | 12 +- app/addons/config/components.js | 433 --------------------- app/addons/config/components/AddOptionButton.js | 129 ++++++ .../config/components/AddOptionButtonContainer.js | 35 ++ app/addons/config/components/ConfigOption.js | 62 +++ app/addons/config/components/ConfigOptionTrash.js | 56 +++ app/addons/config/components/ConfigOptionValue.js | 83 ++++ app/addons/config/components/ConfigTable.js | 66 ++++ .../config/components/ConfigTableContainer.js | 58 +++ app/addons/config/components/ConfigTableScreen.js | 69 ++++ app/addons/config/components/ConfigTabs.js | 51 +++ app/addons/config/layout.js | 24 +- app/addons/config/reducers.js | 172 ++++++++ app/addons/config/resources.js | 70 ---- app/addons/config/routes.js | 31 +- app/addons/config/stores.js | 149 ------- 22 files changed, 1258 insertions(+), 1095 deletions(-) diff --git a/app/addons/config/__tests__/actions.test.js b/app/addons/config/__tests__/actions.test.js index 410dabc..d9e9649 100644 --- a/app/addons/config/__tests__/actions.test.js +++ b/app/addons/config/__tests__/actions.test.js @@ -9,13 +9,13 @@ // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. -import testUtils from "../../../../test/mocha/testUtils"; -import FauxtonAPI from "../../../core/api"; -import Actions from "../actions"; -import Backbone from "backbone"; -import sinon from "sinon"; +import testUtils from '../../../../test/mocha/testUtils'; +import FauxtonAPI from '../../../core/api'; +import * as Actions from '../actions'; +import ActionTypes from '../actiontypes'; +import * as ConfigAPI from '../api'; +import sinon from 'sinon'; -const assert = testUtils.assert; const restore = testUtils.restore; describe('Config Actions', () => { @@ -25,190 +25,121 @@ describe('Config Actions', () => { optionName: 'test', value: 'test' }; - const failXhr = { responseText: '{}' }; + const spySaveConfigOption = sinon.stub(ConfigAPI, 'saveConfigOption'); + const spyDeleteConfigOption = sinon.stub(ConfigAPI, 'deleteConfigOption'); + const dispatch = sinon.stub(); + + describe('addOption', () => { - describe('add', () => { afterEach(() => { - restore(Actions.optionAddSuccess); - restore(Actions.optionAddFailure); - restore(FauxtonAPI.when); + spySaveConfigOption.reset(); + dispatch.reset(); restore(FauxtonAPI.addNotification); - restore(Backbone.Model.prototype.save); - }); - - it('calls optionAddSuccess when option add succeeds', () => { - const stub = sinon.stub(Backbone.Model.prototype, 'save'); - const spy = sinon.spy(Actions, 'optionAddSuccess'); - const promise = FauxtonAPI.Deferred(); - promise.resolve(); - stub.returns(promise); - - return Actions.addOption(node, option) - .then(() => { - assert.ok(spy.calledOnce); - }); - }); - - it('shows notification when option add succeeds', () => { - const stub = sinon.stub(Backbone.Model.prototype, 'save'); - const spy = sinon.spy(FauxtonAPI, 'addNotification'); - const promise = FauxtonAPI.Deferred(); - promise.resolve(); - stub.returns(promise); - - return Actions.addOption(node, option) - .then(() => { - assert.ok(spy.calledOnce); - }); }); - it('calls optionAddFailure when option add fails', () => { - const stub = sinon.stub(Backbone.Model.prototype, 'save'); - const spy = sinon.spy(Actions, 'optionAddFailure'); - const promise = FauxtonAPI.Deferred(); - promise.reject(failXhr); - stub.returns(promise); + it('dispatches OPTION_ADD_SUCCESS and shows notification when option add succeeds', () => { + const promise = FauxtonAPI.Promise.resolve(); + spySaveConfigOption.returns(promise); + const spyAddNotification = sinon.spy(FauxtonAPI, 'addNotification'); - return Actions.addOption(node, option) + return Actions.addOption(node, option)(dispatch) .then(() => { - assert.ok(spy.calledOnce); + sinon.assert.calledWith(dispatch, { + type: ActionTypes.OPTION_ADD_SUCCESS, + options: { optionName: "test", sectionName: "test", value: "test" } + }); + sinon.assert.called(spyAddNotification); }); }); - it('shows notification when option add fails', () => { - const stub = sinon.stub(Backbone.Model.prototype, 'save'); - const spy = sinon.spy(FauxtonAPI, 'addNotification'); - const promise = FauxtonAPI.Deferred(); - promise.reject(failXhr); - stub.returns(promise); + it('dispatches OPTION_ADD_FAILURE and shows notification when option add fails', () => { + const promise = FauxtonAPI.Promise.reject(new Error('')); + spySaveConfigOption.returns(promise); + const spyAddNotification = sinon.spy(FauxtonAPI, 'addNotification'); - return Actions.addOption(node, option) + return Actions.addOption(node, option)(dispatch) .then(() => { - assert.ok(spy.calledOnce); + sinon.assert.calledWith(dispatch, { + type: ActionTypes.OPTION_ADD_FAILURE, + options: { optionName: "test", sectionName: "test", value: "test" } + }); + sinon.assert.called(spyAddNotification); }); }); }); - describe('save', () => { + describe('saveOption', () => { afterEach(() => { - restore(Actions.optionSaveSuccess); - restore(Actions.optionSaveFailure); - restore(FauxtonAPI.when); + spySaveConfigOption.reset(); + dispatch.reset(); restore(FauxtonAPI.addNotification); - restore(Backbone.Model.prototype.save); - }); - - it('calls optionSaveSuccess when option save succeeds', () => { - const stub = sinon.stub(Backbone.Model.prototype, 'save'); - const spy = sinon.spy(Actions, 'optionSaveSuccess'); - const promise = FauxtonAPI.Deferred(); - promise.resolve(); - stub.returns(promise); - - return Actions.saveOption(node, option) - .then(() => { - assert.ok(spy.calledOnce); - }); }); - it('shows notification when option save succeeds', () => { - const stub = sinon.stub(Backbone.Model.prototype, 'save'); - const spy = sinon.spy(FauxtonAPI, 'addNotification'); - const promise = FauxtonAPI.Deferred(); - promise.resolve(); - stub.returns(promise); + it('dispatches OPTION_SAVE_SUCCESS and shows notification when option add succeeds', () => { + const promise = FauxtonAPI.Promise.resolve(); + spySaveConfigOption.returns(promise); + const spyAddNotification = sinon.spy(FauxtonAPI, 'addNotification'); - return Actions.saveOption(node, option) + return Actions.saveOption(node, option)(dispatch) .then(() => { - assert.ok(spy.calledOnce); + sinon.assert.calledWith(dispatch, { + type: ActionTypes.OPTION_SAVE_SUCCESS, + options: { optionName: "test", sectionName: "test", value: "test" } + }); + sinon.assert.called(spyAddNotification); }); }); - it('calls optionSaveFailure when option save fails', () => { - const stub = sinon.stub(Backbone.Model.prototype, 'save'); - const spy = sinon.spy(Actions, 'optionSaveFailure'); - const promise = FauxtonAPI.Deferred(); - promise.reject(failXhr); - stub.returns(promise); + it('dispatches OPTION_SAVE_FAILURE and shows notification when option add fails', () => { + const promise = FauxtonAPI.Promise.reject(new Error('')); + spySaveConfigOption.returns(promise); + const spyAddNotification = sinon.spy(FauxtonAPI, 'addNotification'); - return Actions.saveOption(node, option) + return Actions.saveOption(node, option)(dispatch) .then(() => { - assert.ok(spy.calledOnce); - }); - }); - - it('shows notification when option save fails', () => { - const stub = sinon.stub(Backbone.Model.prototype, 'save'); - const spy = sinon.spy(FauxtonAPI, 'addNotification'); - const promise = FauxtonAPI.Deferred(); - promise.reject(failXhr); - stub.returns(promise); - - return Actions.saveOption(node, option) - .then(() => { - assert.ok(spy.calledOnce); + sinon.assert.calledWith(dispatch, { + type: ActionTypes.OPTION_SAVE_FAILURE, + options: { optionName: "test", sectionName: "test", value: "test" } + }); + sinon.assert.called(spyAddNotification); }); }); }); - describe('delete', () => { + describe('deleteOption', () => { afterEach(() => { - restore(Actions.optionDeleteSuccess); - restore(Actions.optionDeleteFailure); - restore(FauxtonAPI.when); + spyDeleteConfigOption.reset(); + dispatch.reset(); restore(FauxtonAPI.addNotification); - restore(Backbone.Model.prototype.destroy); - }); - - it('calls optionDeleteSuccess when option delete succeeds', () => { - const stub = sinon.stub(Backbone.Model.prototype, 'destroy'); - const spy = sinon.spy(Actions, 'optionDeleteSuccess'); - const promise = FauxtonAPI.Deferred(); - promise.resolve(); - stub.returns(promise); - - return Actions.deleteOption(node, option) - .then(() => { - assert.ok(spy.calledOnce); - }); - }); - - it('shows notification when option delete succeeds', () => { - const stub = sinon.stub(Backbone.Model.prototype, 'destroy'); - const spy = sinon.spy(FauxtonAPI, 'addNotification'); - const promise = FauxtonAPI.Deferred(); - promise.resolve(); - stub.returns(promise); - - return Actions.deleteOption(node, option) - .then(() => { - assert.ok(spy.calledOnce); - }); }); - it('calls optionDeleteFailure when option delete fails', () => { - const stub = sinon.stub(Backbone.Model.prototype, 'destroy'); - const spy = sinon.spy(Actions, 'optionDeleteFailure'); - const promise = FauxtonAPI.Deferred(); - promise.reject(failXhr); - stub.returns(promise); + it('dispatches OPTION_DELETE_SUCCESS and shows notification when option add succeeds', () => { + const promise = FauxtonAPI.Promise.resolve(); + spyDeleteConfigOption.returns(promise); + const spyAddNotification = sinon.spy(FauxtonAPI, 'addNotification'); - return Actions.deleteOption(node, option) + return Actions.deleteOption(node, option)(dispatch) .then(() => { - assert.ok(spy.calledOnce); + sinon.assert.calledWith(dispatch, { + type: ActionTypes.OPTION_DELETE_SUCCESS, + options: { optionName: "test", sectionName: "test", value: "test" } + }); + sinon.assert.called(spyAddNotification); }); }); - it('shows notification when option delete fails', () => { - const stub = sinon.stub(Backbone.Model.prototype, 'destroy'); - const spy = sinon.spy(FauxtonAPI, 'addNotification'); - const promise = FauxtonAPI.Deferred(); - promise.reject(failXhr); - stub.returns(promise); + it('dispatches OPTION_DELETE_FAILURE and shows notification when option add fails', () => { + const promise = FauxtonAPI.Promise.reject(new Error('')); + spyDeleteConfigOption.returns(promise); + const spyAddNotification = sinon.spy(FauxtonAPI, 'addNotification'); - return Actions.deleteOption(node, option) + return Actions.deleteOption(node, option)(dispatch) .then(() => { - assert.ok(spy.calledOnce); + sinon.assert.calledWith(dispatch, { + type: ActionTypes.OPTION_DELETE_FAILURE, + options: { optionName: "test", sectionName: "test", value: "test" } + }); + sinon.assert.called(spyAddNotification); }); }); }); diff --git a/app/addons/config/__tests__/components.test.js b/app/addons/config/__tests__/components.test.js index bd4c515..a250e3e 100644 --- a/app/addons/config/__tests__/components.test.js +++ b/app/addons/config/__tests__/components.test.js @@ -10,85 +10,116 @@ // License for the specific language governing permissions and limitations under // the License. -import FauxtonAPI from "../../../core/api"; -import Views from "../components"; -import Actions from "../actions"; -import Stores from "../stores"; -import utils from "../../../../test/mocha/testUtils"; -import React from "react"; -import ReactDOM from "react-dom"; +import React from 'react'; import {mount} from 'enzyme'; -import sinon from "sinon"; +import sinon from 'sinon'; +import FauxtonAPI from '../../../core/api'; +import AddOptionButton from '../components/AddOptionButton'; +import ConfigOption from '../components/ConfigOption'; +import ConfigOptionValue from '../components/ConfigOptionValue'; +import ConfigOptionTrash from '../components/ConfigOptionTrash'; +import ConfigTableScreen from '../components/ConfigTableScreen'; +import utils from '../../../../test/mocha/testUtils'; FauxtonAPI.router = new FauxtonAPI.Router([]); const assert = utils.assert; -const configStore = Stores.configStore; describe('Config Components', () => { - describe('ConfigTableController', () => { - let elm, node; - - beforeEach(() => { - configStore._loading = false; - configStore._sections = {}; - node = 'node2@127.0.0.1'; - elm = mount( - <Views.ConfigTableController node={node}/> - ); - }); + describe('ConfigTableScreen', () => { + const options = [ + {editing: false, header:true, sectionName: 'sec1', optionName: 'opt1', value: 'value1'}, + {editing: false, header:false, sectionName: 'sec1', optionName: 'opt2', value: 'value2'} + ]; + const node = 'test_node'; + const defaultProps = { + saving: false, + loading: false, + deleteOption: () => {}, + saveOption: () => {}, + editOption: () => {}, + cancelEdit: () => {}, + fetchAndEditConfig: () => {}, + node, + options + }; it('deletes options', () => { - const spy = sinon.stub(Actions, 'deleteOption'); - var option = {}; - - elm.instance().deleteOption(option); - assert.ok(spy.calledWith(node, option)); + const spy = sinon.stub(); + const wrapper = mount(<ConfigTableScreen + {...defaultProps} + deleteOption={spy}/> + ); + wrapper.instance().deleteOption({}); + sinon.assert.called(spy); }); it('saves options', () => { - const spy = sinon.stub(Actions, 'saveOption'); - var option = {}; - - elm.instance().saveOption(option); - assert.ok(spy.calledWith(node, option)); + const spy = sinon.stub(); + const wrapper = mount(<ConfigTableScreen + {...defaultProps} + saveOption={spy}/> + ); + wrapper.instance().saveOption({}); + sinon.assert.called(spy); }); it('edits options', () => { - const spy = sinon.stub(Actions, 'editOption'); - var option = {}; - - elm.instance().editOption(option); - assert.ok(spy.calledWith(option)); + const spy = sinon.stub(); + const wrapper = mount(<ConfigTableScreen + {...defaultProps} + editOption={spy}/> + ); + wrapper.instance().editOption({}); + sinon.assert.called(spy); }); it('cancels editing', () => { - const spy = sinon.stub(Actions, 'cancelEdit'); - - elm.instance().cancelEdit(); - assert.ok(spy.calledOnce); + const spy = sinon.stub(); + const wrapper = mount(<ConfigTableScreen + {...defaultProps} + cancelEdit={spy}/> + ); + wrapper.instance().cancelEdit(); + sinon.assert.called(spy); }); }); describe('ConfigOption', () => { - + const defaultProps = { + option: {}, + saving: false, + onEdit: () => {}, + onCancelEdit: () => {}, + onSave: () => {}, + onDelete: () => {} + }; it('renders section name if the option is a header', () => { const option = { sectionName: 'test_section', optionName: 'test_option', value: 'test_value', - header: true + header: true, + editing: true }; - const el = mount(<table><tbody><Views.ConfigOption option={option}/></tbody></table>); + const el = mount(<table><tbody><ConfigOption {...defaultProps} option={option}/></tbody></table>); assert.equal(el.find('th').text(), 'test_section'); }); }); describe('ConfigOptionValue', () => { + const defaultProps = { + value: '', + editing: false, + onEdit: () => {}, + onCancelEdit: () => {}, + onSave: () => {} + }; + it('displays the value prop', () => { const el = mount( <table><tbody><tr> - <Views.ConfigOptionValue value={'test_value'}/> + <ConfigOptionValue {...defaultProps} value={'test_value'}/> </tr></tbody></table> ); @@ -99,18 +130,18 @@ describe('Config Components', () => { const spy = sinon.spy(); const el = mount( <table><tbody><tr> - <Views.ConfigOptionValue value={'test_value'} onEdit={spy}/> + <ConfigOptionValue {...defaultProps} value={'test_value'} onEdit={spy}/> </tr></tbody></table> ); - el.find(Views.ConfigOptionValue).simulate('click'); + el.find(ConfigOptionValue).simulate('click'); assert.ok(spy.calledOnce); }); it('displays editing controls if editing', () => { const el = mount( <table><tbody><tr> - <Views.ConfigOptionValue value={'test_value'} editing/> + <ConfigOptionValue {...defaultProps} value={'test_value'} editing/> </tr></tbody></table> ); @@ -119,15 +150,13 @@ describe('Config Components', () => { assert.equal(el.find('button.btn-config-save').length, 1); }); - it('disables input when save clicked', () => { + it('disables input when saving is set to true', () => { const el = mount( <table><tbody><tr> - <Views.ConfigOptionValue value={'test_value'} editing/> + <ConfigOptionValue {...defaultProps} value={'test_value'} editing={true} saving={true}/> </tr></tbody></table> ); - el.find('input.config-value-input').simulate('change', {target: {value: 'value'}}); - el.find('button.btn-config-save').simulate('click'); assert.ok(el.find('input.config-value-input').prop('disabled')); }); @@ -136,7 +165,7 @@ describe('Config Components', () => { const spy = sinon.spy(); const el = mount( <table><tbody><tr> - <Views.ConfigOptionValue value={'test_value'} editing onSave={spy}/> + <ConfigOptionValue {...defaultProps} value={'test_value'} editing onSave={spy}/> </tr></tbody></table> ); @@ -149,7 +178,7 @@ describe('Config Components', () => { const spy = sinon.spy(); const el = mount( <table><tbody><tr> - <Views.ConfigOptionValue value={'test_value'} editing onCancelEdit={spy}/> + <ConfigOptionValue {...defaultProps} value={'test_value'} editing onCancelEdit={spy}/> </tr></tbody></table> ); @@ -162,7 +191,7 @@ describe('Config Components', () => { it.skip('displays delete modal when clicked', () => { const el = mount( - <Views.ConfigOptionTrash sectionName='test_section' optionName='test_option'/> + <ConfigOptionTrash sectionName='test_section' optionName='test_option'/> ); el.simulate('click'); @@ -172,7 +201,7 @@ describe('Config Components', () => { it.skip('calls on delete when confirmation modal Okay button clicked', () => { const spy = sinon.spy(); const el = mount( - <Views.ConfigOptionTrash onDelete={spy}/> + <ConfigOptionTrash onDelete={spy}/> ); el.simulate('click'); @@ -181,19 +210,14 @@ describe('Config Components', () => { }); }); - describe('AddOptionController', () => { - let elm; - - beforeEach(() => { - elm = mount( - <Views.AddOptionController node='node2@127.0.0.1'/> - ); - }); - + //we need enzyme to support portals for this + describe.skip('AddOptionButton', () => { it('adds options', () => { - const spy = sinon.stub(Actions, 'addOption'); - - elm.instance().addOption(); + const spy = sinon.stub(); + const wrapper = mount( + <AddOptionButton onAdd={spy}/> + ); + wrapper.instance().onAdd(); assert.ok(spy.calledOnce); }); }); @@ -202,7 +226,7 @@ describe('Config Components', () => { describe.skip('AddOptionButton', () => { it('displays add option controls when clicked', () => { const el = mount( - <Views.AddOptionButton/> + <AddOptionButton/> ); el.find('button#add-option-button').simulate('click'); @@ -214,7 +238,7 @@ describe('Config Components', () => { it('does not hide popover if create clicked with invalid input', () => { const el = mount( - <Views.AddOptionButton/> + <AddOptionButton/> ); el.find('button#add-option-button').simulate('click'); @@ -224,7 +248,7 @@ describe('Config Components', () => { it('does not add option if create clicked with invalid input', () => { const el = mount( - <Views.AddOptionButton/> + <AddOptionButton/> ); el.find('button#add-option-button').simulate('click'); @@ -235,7 +259,7 @@ describe('Config Components', () => { it('does adds option if create clicked with valid input', () => { const el = mount( - <Views.AddOptionButton/> + <AddOptionButton/> ); el.find('button#add-option-button').simulate('click'); @@ -246,7 +270,7 @@ describe('Config Components', () => { it('adds option when create clicked with valid input', () => { const spy = sinon.spy(); const el = mount( - <Views.AddOptionButton onAdd={spy}/> + <AddOptionButton onAdd={spy}/> ); el.find('button#add-option-button').simulate('click'); diff --git a/app/addons/config/__tests__/reducers.test.js b/app/addons/config/__tests__/reducers.test.js new file mode 100644 index 0000000..346b699 --- /dev/null +++ b/app/addons/config/__tests__/reducers.test.js @@ -0,0 +1,114 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import utils from '../../../../test/mocha/testUtils'; +import ActionTypes from '../actiontypes'; +import reducer, {options} from '../reducers'; + +const {assert} = utils; + +describe('Config Reducer', () => { + const editConfigAction = { + type: ActionTypes.EDIT_CONFIG, + options: { + sections: { + test: { b: 1, c: 2, a: 3 } + } + } + }; + describe('fetchConfig', () => { + it('sorts options ascending', () => { + const newState = reducer(undefined, editConfigAction); + assert.ok(options(newState)[0].optionName, 'a'); + }); + + it('sets the first option as the header', () => { + const newState = reducer(undefined, editConfigAction); + assert.isTrue(options(newState)[0].header); + }); + }); + + describe('editOption', () => { + it('sets the option that is being edited', () => { + let newState = reducer(undefined, editConfigAction); + const opts = options(newState); + opts.forEach(el => { + assert.isFalse(el.editing); + }); + + const editOptionAction = { + type: ActionTypes.EDIT_OPTION, + options: { + sectionName: 'test', + optionName: 'b' + } + }; + newState = reducer(newState, editOptionAction); + const opts2 = options(newState); + assert.isTrue(opts2[1].editing); + }); + }); + + describe('saveOption', () => { + it('sets new option value', () => { + let newState = reducer(undefined, editConfigAction); + assert.equal(options(newState)[1].value, '1'); + + const saveOptionAction = { + type: ActionTypes.OPTION_SAVE_SUCCESS, + options: { + sectionName: 'test', + optionName: 'b', + value: 'new_value' + } + }; + newState = reducer(newState, saveOptionAction); + assert.equal(options(newState)[1].value, 'new_value'); + }); + }); + + describe('deleteOption', () => { + it('deletes option from section', () => { + let newState = reducer(undefined, editConfigAction); + assert.equal(options(newState).length, 3); + + const deleteOptionAction = { + type: ActionTypes.OPTION_DELETE_SUCCESS, + options: { + sectionName: 'test', + optionName: 'b' + } + }; + newState = reducer(newState, deleteOptionAction); + assert.equal(options(newState).length, 2); + }); + + it('deletes section when all options are deleted', () => { + let newState = reducer(undefined, editConfigAction); + assert.equal(options(newState).length, 3); + + const deleteOptionAction = { + type: ActionTypes.OPTION_DELETE_SUCCESS, + options: { + sectionName: 'test', + optionName: 'a' + } + }; + newState = reducer(newState, deleteOptionAction); + deleteOptionAction.options.optionName = 'b'; + newState = reducer(newState, deleteOptionAction); + deleteOptionAction.options.optionName = 'c'; + newState = reducer(newState, deleteOptionAction); + assert.equal(options(newState).length, 0); + }); + }); +}); diff --git a/app/addons/config/__tests__/stores.test.js b/app/addons/config/__tests__/stores.test.js deleted file mode 100644 index 98ffb7e..0000000 --- a/app/addons/config/__tests__/stores.test.js +++ /dev/null @@ -1,94 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy of -// the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. - -import Stores from '../stores'; -import utils from '../../../../test/mocha/testUtils'; - -const {assert} = utils; - -describe("ConfigStore", () => { - const configStore = Stores.configStore; - - describe("mapSection", () => { - beforeEach(() => { - configStore._editOptionName = 'b'; - configStore._editSectionName = 'test'; - }); - - afterEach(() => { - configStore.reset(); - }); - - it("sorts options ascending", () => { - const options = configStore.mapSection({ b: 1, c: 2, a: 3 }, 'test'); - assert.equal(options[0].optionName, 'a'); - }); - - it("sets the first option as the header", () => { - const options = configStore.mapSection({ b: 1, c: 2, a: 3 }, 'test'); - assert.isTrue(options[0].header); - }); - - it("sets the option that is being edited", () => { - const options = configStore.mapSection({ b: 1, c: 2, a: 3 }, 'test'); - assert.isTrue(options[1].editing); - }); - }); - - describe("saveOption", () => { - let sectionName, optionName, value; - - beforeEach(() => { - sectionName = 'a'; - optionName = 'b'; - value = 1; - }); - - afterEach(() => { - configStore.reset(); - }); - - it("saves option to sections", () => { - configStore._sections = {}; - - configStore.saveOption(sectionName, optionName, value); - assert.deepEqual(configStore._sections, { a: { b: 1 } }); - }); - }); - - describe("deleteOption", () => { - let sectionName, optionName; - - beforeEach(() => { - sectionName = 'a'; - optionName = 'b'; - }); - - afterEach(() => { - configStore.reset(); - }); - - it("deletes option from section", () => { - configStore._sections = { a: { b: 1, c: 2 } }; - - configStore.deleteOption(sectionName, optionName); - assert.deepEqual(configStore._sections, { a: { c: 2 } }); - }); - - it("deletes section when all options are deleted", () => { - configStore._sections = { a: { b: 1 } }; - - configStore.deleteOption(sectionName, optionName); - assert.deepEqual(configStore._sections, {}); - }); - }); -}); diff --git a/app/addons/config/actions.js b/app/addons/config/actions.js index e633c78..ae4a97d 100644 --- a/app/addons/config/actions.js +++ b/app/addons/config/actions.js @@ -12,114 +12,120 @@ import ActionTypes from './actiontypes'; import FauxtonAPI from '../../core/api'; -import Resources from './resources'; - -export default { - fetchAndEditConfig (node) { - FauxtonAPI.dispatch({ type: ActionTypes.LOADING_CONFIG }); - - var configModel = new Resources.ConfigModel({ node }); - - configModel.fetch().then(() => this.editSections({ sections: configModel.get('sections'), node })); - }, - - editSections (options) { - FauxtonAPI.dispatch({ type: ActionTypes.EDIT_CONFIG, options }); - }, - - editOption (options) { - FauxtonAPI.dispatch({ type: ActionTypes.EDIT_OPTION, options }); - }, - - cancelEdit (options) { - FauxtonAPI.dispatch({ type: ActionTypes.CANCEL_EDIT, options }); - }, - - saveOption (node, options) { - FauxtonAPI.dispatch({ type: ActionTypes.SAVING_OPTION, options }); +import * as ConfigAPI from './api'; + +export const fetchAndEditConfig = (node) => (dispatch) => { + dispatch({ type: ActionTypes.LOADING_CONFIG }); + + ConfigAPI.fetchConfig(node).then(res => { + dispatch({ + type: ActionTypes.EDIT_CONFIG, + options: { + sections: res.sections, + node + } + }); + }).catch(err => { + FauxtonAPI.addNotification({ + msg: 'Failed to load the configuration. ' + err.message, + type: 'error', + clear: true + }); + dispatch({ + type: ActionTypes.EDIT_CONFIG, + options: { + sections: [], + node + } + }); + }); +}; - var modelAttrs = options; - modelAttrs.node = node; - var optionModel = new Resources.OptionModel(modelAttrs); +export const editOption = (options) => (dispatch) => { + dispatch({ type: ActionTypes.EDIT_OPTION, options }); +}; - return optionModel.save() - .then( - () => this.optionSaveSuccess(options), - xhr => this.optionSaveFailure(options, JSON.parse(xhr.responseText).reason) - ); - }, +export const cancelEdit = (options) => (dispatch) => { + dispatch({ type: ActionTypes.CANCEL_EDIT, options }); +}; - optionSaveSuccess (options) { - FauxtonAPI.dispatch({ type: ActionTypes.OPTION_SAVE_SUCCESS, options }); - FauxtonAPI.addNotification({ - msg: `Option ${options.optionName} saved`, - type: 'success' - }); - }, +export const saveOption = (node, options) => (dispatch) => { + dispatch({ type: ActionTypes.SAVING_OPTION, options }); - optionSaveFailure (options, error) { - FauxtonAPI.dispatch({ type: ActionTypes.OPTION_SAVE_FAILURE, options }); - FauxtonAPI.addNotification({ - msg: `Option save failed: ${error}`, - type: 'error' - }); - }, + const { sectionName, optionName, value } = options; + return ConfigAPI.saveConfigOption(node, sectionName, optionName, value).then( + () => optionSaveSuccess(options, dispatch) + ).catch( + (err) => optionSaveFailure(options, err.message, dispatch) + ); +}; - addOption (node, options) { - FauxtonAPI.dispatch({ type: ActionTypes.ADDING_OPTION }); +const optionSaveSuccess = (options, dispatch) => { + dispatch({ type: ActionTypes.OPTION_SAVE_SUCCESS, options }); + FauxtonAPI.addNotification({ + msg: `Option ${options.optionName} saved`, + type: 'success' + }); +}; - var modelAttrs = options; - modelAttrs.node = node; - var optionModel = new Resources.OptionModel(modelAttrs); +const optionSaveFailure = (options, error, dispatch) => { + dispatch({ type: ActionTypes.OPTION_SAVE_FAILURE, options }); + FauxtonAPI.addNotification({ + msg: `Option save failed: ${error}`, + type: 'error' + }); +}; - return optionModel.save() - .then( - () => this.optionAddSuccess(options), - xhr => this.optionAddFailure(options, JSON.parse(xhr.responseText).reason) - ); - }, +export const addOption = (node, options) => (dispatch) => { + dispatch({ type: ActionTypes.ADDING_OPTION }); - optionAddSuccess (options) { - FauxtonAPI.dispatch({ type: ActionTypes.OPTION_ADD_SUCCESS, options }); - FauxtonAPI.addNotification({ - msg: `Option ${options.optionName} added`, - type: 'success' - }); - }, + const { sectionName, optionName, value } = options; + return ConfigAPI.saveConfigOption(node, sectionName, optionName, value).then( + () => optionAddSuccess(options, dispatch) + ).catch( + (err) => optionAddFailure(options, err.message, dispatch) + ); +}; - optionAddFailure (options, error) { - FauxtonAPI.dispatch({ type: ActionTypes.OPTION_ADD_FAILURE, options }); - FauxtonAPI.addNotification({ - msg: `Option add failed: ${error}`, - type: 'error' - }); - }, +const optionAddSuccess = (options, dispatch) => { + dispatch({ type: ActionTypes.OPTION_ADD_SUCCESS, options }); + FauxtonAPI.addNotification({ + msg: `Option ${options.optionName} added`, + type: 'success' + }); +}; - deleteOption (node, options) { - FauxtonAPI.dispatch({ type: ActionTypes.DELETING_OPTION, options }); +const optionAddFailure = (options, error, dispatch) => { + dispatch({ type: ActionTypes.OPTION_ADD_FAILURE, options }); + FauxtonAPI.addNotification({ + msg: `Option add failed: ${error}`, + type: 'error' + }); +}; - var modelAttrs = options; - modelAttrs.node = node; - var optionModel = new Resources.OptionModel(modelAttrs); +export const deleteOption = (node, options) => (dispatch) => { + dispatch({ type: ActionTypes.DELETING_OPTION, options }); - return optionModel.destroy() - .then(() => this.optionDeleteSuccess(options)) - .catch((err) => this.optionDeleteFailure(options, err.message)); - }, + const { sectionName, optionName } = options; + return ConfigAPI.deleteConfigOption(node, sectionName, optionName).then( + () => optionDeleteSuccess(options, dispatch) + ).catch( + (err) => optionDeleteFailure(options, err.message, dispatch) + ); +}; - optionDeleteSuccess (options) { - FauxtonAPI.dispatch({ type: ActionTypes.OPTION_DELETE_SUCCESS, options }); - FauxtonAPI.addNotification({ - msg: `Option ${options.optionName} deleted`, - type: 'success' - }); - }, +const optionDeleteSuccess = (options, dispatch) => { + dispatch({ type: ActionTypes.OPTION_DELETE_SUCCESS, options }); + FauxtonAPI.addNotification({ + msg: `Option ${options.optionName} deleted`, + type: 'success' + }); +}; - optionDeleteFailure (options, error) { - FauxtonAPI.dispatch({ type: ActionTypes.OPTION_DELETE_FAILURE, options }); - FauxtonAPI.addNotification({ - msg: `Option delete failed: ${error}`, - type: 'error' - }); - } +const optionDeleteFailure = (options, error, dispatch) => { + dispatch({ type: ActionTypes.OPTION_DELETE_FAILURE, options }); + FauxtonAPI.addNotification({ + msg: `Option delete failed: ${error}`, + type: 'error' + }); }; diff --git a/app/addons/config/api.js b/app/addons/config/api.js new file mode 100644 index 0000000..5a9f17d --- /dev/null +++ b/app/addons/config/api.js @@ -0,0 +1,54 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import { get, put, deleteRequest } from '../../core/ajax'; +import Helpers from "../../helpers"; + +export const configUrl = (node) => { + return Helpers.getServerUrl('/_node/' + node + '/_config'); +}; + +export const fetchConfig = (node) => { + const url = configUrl(node); + return get(url).then((json) => { + if (json.error) { + throw new Error(json.reason); + } + return { sections: json }; + }); +}; + +export const optionUrl = (node, sectionName, optionName) => { + const endpointUrl = '/_node/' + node + '/_config/' + + encodeURIComponent(sectionName) + '/' + encodeURIComponent(optionName); + return Helpers.getServerUrl(endpointUrl); +}; + +export const saveConfigOption = (node, sectionName, optionName, value) => { + const url = optionUrl(node, sectionName, optionName); + return put(url, value).then((json) => { + if (json.error) { + throw new Error(json.reason || json.error); + } + return json; + }); +}; + +export const deleteConfigOption = (node, sectionName, optionName) => { + const url = optionUrl(node, sectionName, optionName); + return deleteRequest(url).then((json) => { + if (json.error) { + throw new Error(json.reason); + } + return json; + }); +}; diff --git a/app/addons/config/base.js b/app/addons/config/base.js index 2360ed7..ffa6cab 100644 --- a/app/addons/config/base.js +++ b/app/addons/config/base.js @@ -10,9 +10,11 @@ // License for the specific language governing permissions and limitations under // the License. -import FauxtonAPI from "../../core/api"; -import Config from "./routes"; -import "./assets/less/config.less"; +import FauxtonAPI from '../../core/api'; +import Config from './routes'; +import reducers from './reducers'; +import './assets/less/config.less'; + Config.initialize = function () { FauxtonAPI.addHeaderLink({ title: 'Configuration', @@ -22,4 +24,8 @@ Config.initialize = function () { }); }; +FauxtonAPI.addReducers({ + config: reducers +}); + export default Config; diff --git a/app/addons/config/components.js b/app/addons/config/components.js deleted file mode 100644 index 31302f4..0000000 --- a/app/addons/config/components.js +++ /dev/null @@ -1,433 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy of -// the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. - -import PropTypes from 'prop-types'; - -import React from "react"; -import ReactDOM from "react-dom"; -import Stores from "./stores"; -import Actions from "./actions"; -import {Overlay, Button, Popover} from "react-bootstrap"; -import Components from "../components/react-components"; -import FauxtonComponents from "../fauxton/components"; - -const configStore = Stores.configStore; - -class ConfigTableController extends React.Component { - getStoreState = () => { - return { - options: configStore.getOptions(), - loading: configStore.isLoading() - }; - }; - - componentDidMount() { - configStore.on('change', this.onChange, this); - } - - componentWillUnmount() { - configStore.off('change', this.onChange); - } - - onChange = () => { - this.setState(this.getStoreState()); - }; - - saveOption = (option) => { - Actions.saveOption(this.props.node, option); - }; - - deleteOption = (option) => { - Actions.deleteOption(this.props.node, option); - }; - - editOption = (option) => { - Actions.editOption(option); - }; - - cancelEdit = () => { - Actions.cancelEdit(); - }; - - state = this.getStoreState(); - - render() { - if (this.state.loading) { - return ( - <div className="view"> - <Components.LoadLines /> - </div> - ); - } - return ( - <ConfigTable - onDeleteOption={this.deleteOption} - onSaveOption={this.saveOption} - onEditOption={this.editOption} - onCancelEdit={this.cancelEdit} - options={this.state.options}/> - ); - } -} - -class ConfigTable extends React.Component { - createOptions = () => { - return _.map(this.props.options, (option) => ( - <ConfigOption - option={option} - onDelete={this.props.onDeleteOption} - onSave={this.props.onSaveOption} - onEdit={this.props.onEditOption} - onCancelEdit={this.props.onCancelEdit} - key={`${option.sectionName}/${option.optionName}`} - /> - )); - }; - - render() { - var options = this.createOptions(); - - return ( - <table className="config table table-striped table-bordered"> - <thead> - <tr> - <th id="config-section" width="22%">Section</th> - <th id="config-option" width="22%">Option</th> - <th id="config-value">Value</th> - <th id="config-trash"></th> - </tr> - </thead> - <tbody> - {options} - </tbody> - </table> - ); - } -} - -class ConfigOption extends React.Component { - onSave = (value) => { - var option = this.props.option; - option.value = value; - this.props.onSave(option); - }; - - onDelete = () => { - this.props.onDelete(this.props.option); - }; - - onEdit = () => { - this.props.onEdit(this.props.option); - }; - - render() { - return ( - <tr className="config-item"> - <th>{this.props.option.header && this.props.option.sectionName}</th> - <td>{this.props.option.optionName}</td> - <ConfigOptionValue - value={this.props.option.value} - editing={this.props.option.editing} - onSave={this.onSave} - onEdit={this.onEdit} - onCancelEdit={this.props.onCancelEdit} - /> - <ConfigOptionTrash - optionName={this.props.option.optionName} - sectionName={this.props.option.sectionName} - onDelete={this.onDelete}/> - </tr> - ); - } -} - -class ConfigOptionValue extends React.Component { - static defaultProps = { - value: '', - editing: false, - saving: false, - onSave: () => null, - onEdit: () => null, - onCancelEdit: () => null - }; - - state = { - value: this.props.value, - editing: this.props.editing, - saving: this.props.saving - }; - - UNSAFE_componentWillReceiveProps(nextProps) { - if (this.props.value !== nextProps.value) { - this.setState({ saving: false }); - } - } - - onChange = (event) => { - this.setState({ value: event.target.value }); - }; - - onSave = () => { - if (this.state.value !== this.props.value) { - this.setState({ saving: true }); - this.props.onSave(this.state.value); - } else { - this.props.onCancelEdit(); - } - }; - - getButtons = () => { - if (this.state.saving) { - return null; - } - return ( - <span> - <button - className="btn btn-primary fonticon-ok-circled btn-small btn-config-save" - onClick={this.onSave.bind(this)} - /> - <button - className="btn fonticon-cancel-circled btn-small btn-config-cancel" - onClick={this.props.onCancelEdit} - /> - </span> - ); - - }; - - render() { - if (this.props.editing) { - return ( - <td> - <div className="config-value-form"> - <input - onChange={this.onChange.bind(this)} - defaultValue={this.props.value} - disabled={this.state.saving} - autoFocus type="text" className="config-value-input" - /> - {this.getButtons()} - </div> - </td> - ); - } - return ( - <td className="config-show-value" onClick={this.props.onEdit}> - {this.props.value} - </td> - ); - - } -} - -class ConfigOptionTrash extends React.Component { - constructor (props) { - super(props); - this.onDelete = this.onDelete.bind(this); - this.showModal = this.showModal.bind(this); - this.hideModal = this.hideModal.bind(this); - this.state = { show: false }; - } - - onDelete = () => { - this.props.onDelete(); - }; - - showModal = () => { - this.setState({ show: true }); - }; - - hideModal = () => { - this.setState({ show: false }); - }; - - render() { - return ( - <td className="text-center config-item-trash config-delete-value"> - <i className="icon icon-trash" onClick={this.showModal}></i> - <FauxtonComponents.ConfirmationModal - text={`Are you sure you want to delete ${this.props.sectionName}/${this.props.optionName}?`} - onClose={this.hideModal} - onSubmit={this.onDelete} - visible={this.state.show}/> - </td> - ); - } -} - -class AddOptionController extends React.Component { - addOption = (option) => { - Actions.addOption(this.props.node, option); - }; - - render() { - return ( - <AddOptionButton onAdd={this.addOption}/> - ); - } -} - -class AddOptionButton extends React.Component { - constructor(props) { - super(props); - this.state = this.getInitialState(); - } - - getInitialState () { - return { - sectionName: '', - optionName: '', - value: '', - show: false - }; - } - - isInputValid () { - if (this.state.sectionName !== '' - && this.state.optionName !== '' - && this.state.value !== '') { - return true; - } - - return false; - } - - updateSectionName (event) { - this.setState({ sectionName: event.target.value }); - } - - updateOptionName (event) { - this.setState({ optionName: event.target.value }); - } - - updateValue (event) { - this.setState({ value: event.target.value }); - } - - reset () { - this.setState(this.getInitialState()); - } - - onAdd () { - if (this.isInputValid()) { - var option = { - sectionName: this.state.sectionName, - optionName: this.state.optionName, - value: this.state.value - }; - - this.setState({ show: false }); - this.props.onAdd(option); - } - } - - togglePopover () { - this.setState({ show: !this.state.show }); - } - - hidePopover () { - this.setState({ show: false }); - } - - getPopover () { - return ( - <Popover className="tray" id="add-option-popover" title="Add Option"> - <input - className="input-section-name" - onChange={this.updateSectionName.bind(this)} - type="text" name="section" placeholder="Section" autoComplete="off" autoFocus/> - <input - className="input-option-name" - onChange={this.updateOptionName.bind(this)} - type="text" name="name" placeholder="Name"/> - <input - className="input-value" - onChange={this.updateValue.bind(this)} - type="text" name="value" placeholder="Value"/> - <a - className="btn btn-create" - onClick={this.onAdd.bind(this)}> - Create - </a> - </Popover> - ); - } - - render () { - return ( - <div id="add-option-panel"> - <Button - id="add-option-button" - onClick={this.togglePopover.bind(this)} - ref={node => this.target = node}> - <i className="icon icon-plus header-icon"></i> - Add Option - </Button> - - <Overlay - show={this.state.show} - onHide={this.hidePopover.bind(this)} - placement="bottom" - rootClose={true} - target={() => this.target}> - {this.getPopover()} - </Overlay> - </div> - ); - } -} - -const TabItem = ({active, link, title}) => { - return ( - <li className={active ? 'active' : ''}> - <a href={`#${link}`}> - {title} - </a> - </li> - ); -}; - -TabItem.propTypes = { - active: PropTypes.bool.isRequired, - link: PropTypes.string.isRequired, - icon: PropTypes.string, - title: PropTypes.string.isRequired -}; - -const Tabs = ({sidebarItems, selectedTab}) => { - const tabItems = sidebarItems.map(item => { - return <TabItem - key={item.title} - active={selectedTab === item.title} - title={item.title} - link={item.link} - />; - }); - return ( - <nav className="sidenav"> - <ul className="nav nav-list"> - {tabItems} - </ul> - </nav> - ); -}; - -export default { - Tabs, - ConfigTableController, - ConfigTable, - ConfigOption, - ConfigOptionValue, - ConfigOptionTrash, - AddOptionController, - AddOptionButton, -}; diff --git a/app/addons/config/components/AddOptionButton.js b/app/addons/config/components/AddOptionButton.js new file mode 100644 index 0000000..ddaf479 --- /dev/null +++ b/app/addons/config/components/AddOptionButton.js @@ -0,0 +1,129 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import PropTypes from 'prop-types'; +import React from 'react'; +import {Button, Overlay, Popover} from 'react-bootstrap'; + +export default class AddOptionButton extends React.Component { + static propTypes = { + onAdd: PropTypes.func.isRequired + }; + + constructor(props) { + super(props); + this.state = this.getInitialState(); + } + + getInitialState () { + return { + sectionName: '', + optionName: '', + value: '', + show: false + }; + } + + isInputValid () { + if (this.state.sectionName !== '' + && this.state.optionName !== '' + && this.state.value !== '') { + return true; + } + + return false; + } + + updateSectionName (event) { + this.setState({ sectionName: event.target.value }); + } + + updateOptionName (event) { + this.setState({ optionName: event.target.value }); + } + + updateValue (event) { + this.setState({ value: event.target.value }); + } + + reset () { + this.setState(this.getInitialState()); + } + + onAdd () { + if (this.isInputValid()) { + var option = { + sectionName: this.state.sectionName, + optionName: this.state.optionName, + value: this.state.value + }; + + this.setState({ show: false }); + this.props.onAdd(option); + } + } + + togglePopover () { + this.setState({ show: !this.state.show }); + } + + hidePopover () { + this.setState({ show: false }); + } + + getPopover () { + return ( + <Popover className="tray" id="add-option-popover" title="Add Option"> + <input + className="input-section-name" + onChange={this.updateSectionName.bind(this)} + type="text" name="section" placeholder="Section" autoComplete="off" autoFocus/> + <input + className="input-option-name" + onChange={this.updateOptionName.bind(this)} + type="text" name="name" placeholder="Name"/> + <input + className="input-value" + onChange={this.updateValue.bind(this)} + type="text" name="value" placeholder="Value"/> + <a + className="btn btn-create" + onClick={this.onAdd.bind(this)}> + Create + </a> + </Popover> + ); + } + + render () { + return ( + <div id="add-option-panel"> + <Button + id="add-option-button" + onClick={this.togglePopover.bind(this)} + ref={node => this.target = node}> + <i className="icon icon-plus header-icon"></i> + Add Option + </Button> + + <Overlay + show={this.state.show} + onHide={this.hidePopover.bind(this)} + placement="bottom" + rootClose={true} + target={() => this.target}> + {this.getPopover()} + </Overlay> + </div> + ); + } +} diff --git a/app/addons/config/components/AddOptionButtonContainer.js b/app/addons/config/components/AddOptionButtonContainer.js new file mode 100644 index 0000000..331969a --- /dev/null +++ b/app/addons/config/components/AddOptionButtonContainer.js @@ -0,0 +1,35 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import { connect } from 'react-redux'; +import * as Actions from '../actions'; +import AddOptionButton from './AddOptionButton'; + + +const mapStateToProps = () => { + return {}; +}; + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + onAdd: (options) => { + dispatch(Actions.addOption(ownProps.node, options)); + } + }; +}; + +const AddOptionButtonContainer = connect( + mapStateToProps, + mapDispatchToProps +)(AddOptionButton); + +export default AddOptionButtonContainer; diff --git a/app/addons/config/components/ConfigOption.js b/app/addons/config/components/ConfigOption.js new file mode 100644 index 0000000..858eeb5 --- /dev/null +++ b/app/addons/config/components/ConfigOption.js @@ -0,0 +1,62 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import PropTypes from 'prop-types'; +import React from 'react'; +import ConfigOptionValue from './ConfigOptionValue'; +import ConfigOptionTrash from './ConfigOptionTrash'; + +export default class ConfigOption extends React.Component { + static propTypes = { + option: PropTypes.object.isRequired, + saving: PropTypes.bool.isRequired, + onSave: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired, + onEdit: PropTypes.func.isRequired, + onCancelEdit: PropTypes.func.isRequired + }; + + onSave = (value) => { + const option = this.props.option; + option.value = value; + this.props.onSave(option); + }; + + onDelete = () => { + this.props.onDelete(this.props.option); + }; + + onEdit = () => { + this.props.onEdit(this.props.option); + }; + + render() { + return ( + <tr className="config-item"> + <th>{this.props.option.header && this.props.option.sectionName}</th> + <td>{this.props.option.optionName}</td> + <ConfigOptionValue + value={this.props.option.value} + editing={this.props.option.editing} + saving={this.props.saving} + onSave={this.onSave} + onEdit={this.onEdit} + onCancelEdit={this.props.onCancelEdit} + /> + <ConfigOptionTrash + optionName={this.props.option.optionName} + sectionName={this.props.option.sectionName} + onDelete={this.onDelete}/> + </tr> + ); + } +} diff --git a/app/addons/config/components/ConfigOptionTrash.js b/app/addons/config/components/ConfigOptionTrash.js new file mode 100644 index 0000000..a37f031 --- /dev/null +++ b/app/addons/config/components/ConfigOptionTrash.js @@ -0,0 +1,56 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import PropTypes from 'prop-types'; +import React from 'react'; +import FauxtonComponents from '../../fauxton/components'; + +export default class ConfigOptionTrash extends React.Component { + constructor (props) { + super(props); + this.onDelete = this.onDelete.bind(this); + this.showModal = this.showModal.bind(this); + this.hideModal = this.hideModal.bind(this); + this.state = { show: false }; + } + + static propTypes = { + sectionName: PropTypes.string.isRequired, + optionName: PropTypes.string.isRequired, + onDelete: PropTypes.func.isRequired + }; + + onDelete = () => { + this.props.onDelete(); + }; + + showModal = () => { + this.setState({ show: true }); + }; + + hideModal = () => { + this.setState({ show: false }); + }; + + render() { + return ( + <td className="text-center config-item-trash config-delete-value"> + <i className="icon icon-trash" onClick={this.showModal}></i> + <FauxtonComponents.ConfirmationModal + text={`Are you sure you want to delete ${this.props.sectionName}/${this.props.optionName}?`} + onClose={this.hideModal} + onSubmit={this.onDelete} + visible={this.state.show}/> + </td> + ); + } +} diff --git a/app/addons/config/components/ConfigOptionValue.js b/app/addons/config/components/ConfigOptionValue.js new file mode 100644 index 0000000..dc46aaf --- /dev/null +++ b/app/addons/config/components/ConfigOptionValue.js @@ -0,0 +1,83 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import PropTypes from 'prop-types'; +import React from 'react'; + +export default class ConfigOptionValue extends React.Component { + static propTypes = { + value: PropTypes.string.isRequired, + editing: PropTypes.bool.isRequired, + onEdit: PropTypes.func.isRequired, + onCancelEdit: PropTypes.func.isRequired, + onSave: PropTypes.func.isRequired + }; + + state = { + value: this.props.value + }; + + onChange = (event) => { + this.setState({ value: event.target.value }); + }; + + onSave = () => { + if (this.state.value !== this.props.value) { + this.props.onSave(this.state.value); + } else { + this.props.onCancelEdit(); + } + }; + + getButtons = () => { + if (this.props.saving) { + return null; + } + return ( + <span> + <button + className="btn btn-primary fonticon-ok-circled btn-small btn-config-save" + onClick={this.onSave.bind(this)} + /> + <button + className="btn fonticon-cancel-circled btn-small btn-config-cancel" + onClick={this.props.onCancelEdit} + /> + </span> + ); + + }; + + render() { + if (this.props.editing) { + return ( + <td> + <div className="config-value-form"> + <input + onChange={this.onChange.bind(this)} + defaultValue={this.props.value} + disabled={this.props.saving} + autoFocus type="text" className="config-value-input" + /> + {this.getButtons()} + </div> + </td> + ); + } + return ( + <td className="config-show-value" onClick={this.props.onEdit}> + {this.props.value} + </td> + ); + + } +} diff --git a/app/addons/config/components/ConfigTable.js b/app/addons/config/components/ConfigTable.js new file mode 100644 index 0000000..5e7a57d --- /dev/null +++ b/app/addons/config/components/ConfigTable.js @@ -0,0 +1,66 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import PropTypes from 'prop-types'; +import React from 'react'; +import ConfigOption from './ConfigOption'; + +export default class ConfigTable extends React.Component { + static propTypes = { + options: PropTypes.arrayOf(PropTypes.shape({ + editing: PropTypes.bool.isRequired, + header: PropTypes.bool, + optionName: PropTypes.string.isRequired, + sectionName: PropTypes.string.isRequired, + value: PropTypes.string + })).isRequired, + saving: PropTypes.bool.isRequired, + onDeleteOption: PropTypes.func.isRequired, + onEditOption: PropTypes.func.isRequired, + onSaveOption: PropTypes.func.isRequired, + onCancelEdit: PropTypes.func.isRequired + }; + + createOptions = () => { + return _.map(this.props.options, (option) => ( + <ConfigOption + option={option} + saving={this.props.saving} + onDelete={this.props.onDeleteOption} + onSave={this.props.onSaveOption} + onEdit={this.props.onEditOption} + onCancelEdit={this.props.onCancelEdit} + key={`${option.sectionName}/${option.optionName}`} + /> + )); + }; + + render() { + const options = this.createOptions(); + + return ( + <table className="config table table-striped table-bordered"> + <thead> + <tr> + <th id="config-section" width="22%">Section</th> + <th id="config-option" width="22%">Option</th> + <th id="config-value">Value</th> + <th id="config-trash"></th> + </tr> + </thead> + <tbody> + {options} + </tbody> + </table> + ); + } +} diff --git a/app/addons/config/components/ConfigTableContainer.js b/app/addons/config/components/ConfigTableContainer.js new file mode 100644 index 0000000..34750cf --- /dev/null +++ b/app/addons/config/components/ConfigTableContainer.js @@ -0,0 +1,58 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import { connect } from 'react-redux'; +import ConfigTableScreen from './ConfigTableScreen'; +import * as Actions from '../actions'; +import { options } from '../reducers'; + +const mapStateToProps = ({ config }, ownProps) => { + return { + node: ownProps.node, + options: options(config), + loading: config.loading, + saving: config.saving, + editSectionName: config.editSectionName, + editOptionName: config.editOptionName, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + fetchAndEditConfig: (node) => { + dispatch(Actions.fetchAndEditConfig(node)); + }, + + saveOption: (node, options) => { + dispatch(Actions.saveOption(node, options)); + }, + + deleteOption: (node, options) => { + dispatch(Actions.deleteOption(node, options)); + }, + + editOption: (options) => { + dispatch(Actions.editOption(options)); + }, + + cancelEdit: (options) => { + dispatch(Actions.cancelEdit(options)); + } + }; +}; + +const ConfigTableContainer = connect( + mapStateToProps, + mapDispatchToProps +)(ConfigTableScreen); + +export default ConfigTableContainer; diff --git a/app/addons/config/components/ConfigTableScreen.js b/app/addons/config/components/ConfigTableScreen.js new file mode 100644 index 0000000..de4971e --- /dev/null +++ b/app/addons/config/components/ConfigTableScreen.js @@ -0,0 +1,69 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import PropTypes from 'prop-types'; +import React from 'react'; +import Components from '../../components/react-components'; +import ConfigTable from './ConfigTable'; + +export default class ConfigTableScreen extends React.Component { + static propTypes = { + options: PropTypes.array.isRequired, + loading: PropTypes.bool.isRequired, + saving: PropTypes.bool.isRequired, + saveOption: PropTypes.func.isRequired, + deleteOption: PropTypes.func.isRequired, + editOption: PropTypes.func.isRequired, + cancelEdit: PropTypes.func.isRequired, + fetchAndEditConfig: PropTypes.func.isRequired + }; + + constructor(props) { + super(props); + this.props.fetchAndEditConfig(this.props.node); + } + + saveOption = (option) => { + this.props.saveOption(this.props.node, option); + }; + + deleteOption = (option) => { + this.props.deleteOption(this.props.node, option); + }; + + editOption = (option) => { + this.props.editOption(option); + }; + + cancelEdit = () => { + this.props.cancelEdit(); + }; + + render() { + if (this.props.loading) { + return ( + <div className="view"> + <Components.LoadLines /> + </div> + ); + } + return ( + <ConfigTable + saving={this.props.saving} + onDeleteOption={this.deleteOption} + onSaveOption={this.saveOption} + onEditOption={this.editOption} + onCancelEdit={this.cancelEdit} + options={this.props.options}/> + ); + } +} diff --git a/app/addons/config/components/ConfigTabs.js b/app/addons/config/components/ConfigTabs.js new file mode 100644 index 0000000..beab5ee --- /dev/null +++ b/app/addons/config/components/ConfigTabs.js @@ -0,0 +1,51 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import PropTypes from 'prop-types'; +import React from 'react'; + +const ConfigTabs = ({sidebarItems, selectedTab}) => { + const tabItems = sidebarItems.map(item => { + return <TabItem + key={item.title} + active={selectedTab === item.title} + title={item.title} + link={item.link} + />; + }); + return ( + <nav className="sidenav"> + <ul className="nav nav-list"> + {tabItems} + </ul> + </nav> + ); +}; + +const TabItem = ({active, link, title}) => { + return ( + <li className={active ? 'active' : ''}> + <a href={`#${link}`}> + {title} + </a> + </li> + ); +}; + +TabItem.propTypes = { + active: PropTypes.bool.isRequired, + link: PropTypes.string.isRequired, + icon: PropTypes.string, + title: PropTypes.string.isRequired +}; + +export default ConfigTabs; diff --git a/app/addons/config/layout.js b/app/addons/config/layout.js index f3ff87b..87471b4 100644 --- a/app/addons/config/layout.js +++ b/app/addons/config/layout.js @@ -11,23 +11,25 @@ // the License. import React from 'react'; -import ConfigComponents from "./components"; -import CORSComponents from "../cors/components"; -import {Breadcrumbs} from '../components/header-breadcrumbs'; -import {NotificationCenterButton} from '../fauxton/notifications/notifications'; -import {ApiBarWrapper} from '../components/layouts'; +import AddOptionButtonContainer from './components/AddOptionButtonContainer'; +import ConfigTableContainer from './components/ConfigTableContainer'; +import ConfigTabs from './components/ConfigTabs'; +import CORSComponents from '../cors/components'; +import { Breadcrumbs } from '../components/header-breadcrumbs'; +import { NotificationCenterButton } from '../fauxton/notifications/notifications'; +import { ApiBarWrapper } from '../components/layouts'; -export const ConfigHeader = ({node, crumbs, docURL, endpoint}) => { +export const ConfigHeader = ({ node, crumbs, docURL, endpoint }) => { return ( <header className="two-panel-header"> <div className="flex-layout flex-row"> <div id='breadcrumbs' className="faux__config-breadcrumbs"> - <Breadcrumbs crumbs={crumbs}/> + <Breadcrumbs crumbs={crumbs} /> </div> <div className="right-header-wrapper flex-layout flex-row flex-body"> <div id="react-headerbar" className="flex-body"> </div> <div id="right-header" className="flex-fill"> - <ConfigComponents.AddOptionController node={node} /> + <AddOptionButtonContainer node={node} /> </div> <ApiBarWrapper docURL={docURL} endpoint={endpoint} /> <div id="notification-center-btn" className="flex-fill"> @@ -39,7 +41,7 @@ export const ConfigHeader = ({node, crumbs, docURL, endpoint}) => { ); }; -export const ConfigLayout = ({showCors, docURL, node, endpoint, crumbs}) => { +export const ConfigLayout = ({ showCors, docURL, node, endpoint, crumbs }) => { const sidebarItems = [ { title: 'Main config', @@ -51,7 +53,7 @@ export const ConfigLayout = ({showCors, docURL, node, endpoint, crumbs}) => { } ]; const selectedTab = showCors ? 'CORS' : 'Main config'; - const content = showCors ? <CORSComponents.CORSContainer node={node} url={endpoint}/> : <ConfigComponents.ConfigTableController node={node} />; + const content = showCors ? <CORSComponents.CORSContainer node={node} url={endpoint} /> : <ConfigTableContainer node={node} />; return ( <div id="dashboard" className="with-sidebar"> <ConfigHeader @@ -62,7 +64,7 @@ export const ConfigLayout = ({showCors, docURL, node, endpoint, crumbs}) => { /> <div className="with-sidebar tabs-with-sidebar content-area"> <aside id="sidebar-content" className="scrollable"> - <ConfigComponents.Tabs + <ConfigTabs sidebarItems={sidebarItems} selectedTab={selectedTab} /> diff --git a/app/addons/config/reducers.js b/app/addons/config/reducers.js new file mode 100644 index 0000000..4952fb1 --- /dev/null +++ b/app/addons/config/reducers.js @@ -0,0 +1,172 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import ActionTypes from './actiontypes'; + +const initialState = { + sections: {}, + loading: true, + editSectionName: null, + editOptionName: null, + saving: false +}; + +function saveOption(state, { sectionName, optionName, value }) { + const newSections = { + ...state.sections + }; + + if (!newSections[sectionName]) { + newSections[sectionName] = {}; + } + + newSections[sectionName][optionName] = value || true; + return newSections; +} + +function deleteOption(state, { sectionName, optionName }) { + const newSections = { + ...state.sections + }; + + if (newSections[sectionName]) { + // copy object + newSections[sectionName] = {...newSections[sectionName]}; + delete newSections[sectionName][optionName]; + + if (Object.keys(newSections[sectionName]).length == 0) { + delete newSections[sectionName]; + } + } + return newSections; +} + +export function options(state) { + const sections = Object.keys(state.sections).map(sectionName => { + return { + sectionName, + options: mapSection(state, sectionName) + }; + }); + const sortedSections = sections.sort((a, b) => { + if (a.sectionName < b.sectionName) return -1; + else if (a.sectionName > b.sectionName) return 1; + return 0; + }); + // flatten the list of options + return sortedSections.map(s => s.options).reduce((acc, options) => { + return acc.concat(options); + }, []); +} + +function mapSection(state, sectionName) { + const section = state.sections[sectionName]; + const options = Object.keys(section).map(optionName => { + return { + editing: isEditing(state, sectionName, optionName), + sectionName, + optionName, + value: section[optionName] + }; + }); + const sortedOptions = options.sort((a, b) => { + if (a.optionName < b.optionName) return -1; + else if (a.optionName > b.optionName) return 1; + return 0; + }); + if (sortedOptions.length > 0) { + sortedOptions[0].header = true; + } + return sortedOptions; +} + +function isEditing(state, sn, on) { + return sn === state.editSectionName && on === state.editOptionName; +} + +export default function config(state = initialState, action) { + const { options } = action; + + switch (action.type) { + case ActionTypes.EDIT_CONFIG: + return { + ...state, + sections: options.sections, + loading: false, + editOptionName: null, + editSectionName: null + }; + + case ActionTypes.EDIT_OPTION: + return { + ...state, + editSectionName: options.sectionName, + editOptionName: options.optionName + }; + + case ActionTypes.LOADING_CONFIG: + return { + ...state, + loading: true + }; + + case ActionTypes.CANCEL_EDIT: + return { + ...state, + editOptionName: null, + editSectionName: null + }; + + case ActionTypes.SAVING_OPTION: + return { + ...state, + saving: true + }; + + case ActionTypes.OPTION_SAVE_SUCCESS: + return { + ...state, + editOptionName: null, + editSectionName: null, + sections: saveOption(state, options), + saving: false + }; + + case ActionTypes.OPTION_SAVE_FAILURE: + return { + ...state, + saving: false + }; + + case ActionTypes.OPTION_ADD_SUCCESS: + return { + ...state, + sections: saveOption(state, options), + saving: false + }; + + case ActionTypes.OPTION_ADD_FAILURE: + return { + ...state, + saving: false + }; + + case ActionTypes.OPTION_DELETE_SUCCESS: + return { + ...state, + sections: deleteOption(state, options) + }; + + default: + return state; + } +} diff --git a/app/addons/config/resources.js b/app/addons/config/resources.js deleted file mode 100644 index cd4401d..0000000 --- a/app/addons/config/resources.js +++ /dev/null @@ -1,70 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy of -// the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. - -import FauxtonAPI from "../../core/api"; -import Helpers from "../../helpers"; -import { deleteRequest, put } from "../../core/ajax"; - -var Config = FauxtonAPI.addon(); - -Config.OptionModel = Backbone.Model.extend({ - documentation: FauxtonAPI.constants.DOC_URLS.CONFIG, - - url () { - if (!this.get('node')) { - throw new Error('no node set'); - } - const endpointUrl = '/_node/' + this.get('node') + '/_config/' + - this.get('sectionName') + '/' + encodeURIComponent(this.get('optionName')); - return Helpers.getServerUrl(endpointUrl); - }, - - isNew () { return false; }, - - sync (method, model) { - let operation; - if (method === 'delete') { - operation = deleteRequest( - model.url() - ); - } else { - operation = put( - model.url(), - model.get('value') - ); - } - - return operation.then((res) => { - if (res.error) { - throw new Error(res.reason || res.error); - } - return res; - }); - } -}); - -Config.ConfigModel = Backbone.Model.extend({ - documentation: FauxtonAPI.constants.DOC_URLS.CONFIG, - - url () { - if (!this.get('node')) { - throw new Error('no node set'); - } - return Helpers.getServerUrl('/_node/' + this.get('node') + '/_config'); - }, - - parse (resp) { - return { sections: resp }; - } -}); - -export default Config; diff --git a/app/addons/config/routes.js b/app/addons/config/routes.js index 4a21749..fbc6220 100644 --- a/app/addons/config/routes.js +++ b/app/addons/config/routes.js @@ -11,14 +11,12 @@ // the License. import React from 'react'; -import FauxtonAPI from "../../core/api"; -import Config from "./resources"; -import ClusterActions from "../cluster/actions"; -import ConfigActions from "./actions"; +import FauxtonAPI from '../../core/api'; +import ClusterActions from '../cluster/actions'; +import * as ConfigAPI from './api'; import Layout from './layout'; - -var ConfigDisabledRouteObject = FauxtonAPI.RouteObject.extend({ +const ConfigDisabledRouteObject = FauxtonAPI.RouteObject.extend({ selectedHeader: 'Configuration', routes: { @@ -35,31 +33,23 @@ var ConfigDisabledRouteObject = FauxtonAPI.RouteObject.extend({ }); -var ConfigPerNodeRouteObject = FauxtonAPI.RouteObject.extend({ +const ConfigPerNodeRouteObject = FauxtonAPI.RouteObject.extend({ roles: ['_admin'], selectedHeader: 'Configuration', - apiUrl: function () { - return [this.configs.url(), this.configs.documentation]; - }, - routes: { '_config/:node': 'configForNode', '_config/:node/cors': 'configCorsForNode' }, - initialize: function (_a, options) { - var node = options[0]; - - this.configs = new Config.ConfigModel({ node: node }); + initialize: function () { }, configForNode: function (node) { - ConfigActions.fetchAndEditConfig(node); return <Layout node={node} - docURL={this.configs.documentation} - endpoint={this.configs.url()} + docURL={FauxtonAPI.constants.DOC_URLS.CONFIG} + endpoint={ConfigAPI.configUrl(node)} crumbs={[{ name: 'Config' }]} showCors={false} />; @@ -68,14 +58,15 @@ var ConfigPerNodeRouteObject = FauxtonAPI.RouteObject.extend({ configCorsForNode: function (node) { return <Layout node={node} - docURL={this.configs.documentation} - endpoint={this.configs.url()} + docURL={FauxtonAPI.constants.DOC_URLS.CONFIG} + endpoint={ConfigAPI.configUrl(node)} crumbs={[{ name: 'Config' }]} showCors={true} />; } }); +const Config = FauxtonAPI.addon(); Config.RouteObjects = [ConfigPerNodeRouteObject, ConfigDisabledRouteObject]; export default Config; diff --git a/app/addons/config/stores.js b/app/addons/config/stores.js deleted file mode 100644 index c97f206..0000000 --- a/app/addons/config/stores.js +++ /dev/null @@ -1,149 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy of -// the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. - -import FauxtonAPI from '../../core/api'; -import ActionTypes from './actiontypes'; - -var ConfigStore = FauxtonAPI.Store.extend({ - initialize () { - this.reset(); - }, - - reset () { - this._sections = {}; - this._loading = true; - this._editSectionName = null; - this._editOptionName = null; - }, - - editConfig (sections) { - this._sections = sections; - this._loading = false; - this._editSectionName = null; - this._editOptionName = null; - }, - - getOptions () { - var sections = _.sortBy( - _.map(this._sections, (section, sectionName) => { - return { - sectionName, - options: this.mapSection(section, sectionName) - }; - }), - s => s.sectionName - ); - - return _.flatten(_.map(sections, s => s.options)); - }, - - mapSection (section, sectionName) { - var options = _.sortBy( - _.map(section, (value, optionName) => ({ - editing: this.isEditing(sectionName, optionName), - sectionName, optionName, value - })), o => o.optionName - ); - - options[0].header = true; - - return options; - }, - - editOption (sn, on) { - this._editSectionName = sn; - this._editOptionName = on; - }, - - isEditing (sn, on) { - return sn == this._editSectionName && on == this._editOptionName; - }, - - stopEditing () { - this._editOptionName = null; - this._editSectionName = null; - }, - - setLoading () { - this._loading = true; - }, - - isLoading () { - return this._loading; - }, - - saveOption (sectionName, optionName, value) { - if (!this._sections[sectionName]) { - this._sections[sectionName] = {}; - } - - this._sections[sectionName][optionName] = value || true; - }, - - deleteOption (sectionName, optionName) { - if (this._sections[sectionName]) { - delete this._sections[sectionName][optionName]; - - if (Object.keys(this._sections[sectionName]).length == 0) { - delete this._sections[sectionName]; - } - } - }, - - dispatch (action) { - if (action.options) { - var sectionName = action.options.sectionName; - var optionName = action.options.optionName; - var value = action.options.value; - } - - switch (action.type) { - case ActionTypes.EDIT_CONFIG: - this.editConfig(action.options.sections, action.options.node); - break; - - case ActionTypes.LOADING_CONFIG: - this.setLoading(); - break; - - case ActionTypes.EDIT_OPTION: - this.editOption(sectionName, optionName); - break; - - case ActionTypes.CANCEL_EDIT: - this.stopEditing(); - break; - - case ActionTypes.OPTION_SAVE_SUCCESS: - this.saveOption(sectionName, optionName, value); - this.stopEditing(); - break; - - case ActionTypes.OPTION_ADD_SUCCESS: - this.saveOption(sectionName, optionName, value); - break; - - case ActionTypes.OPTION_DELETE_SUCCESS: - this.deleteOption(sectionName, optionName); - break; - } - - this.triggerChange(); - } -}); - -var configStore = new ConfigStore(); -configStore.dispatchToken = FauxtonAPI.dispatcher.register(configStore.dispatch.bind(configStore)); - -export default { - configStore: configStore -};