Update config addon to use redux (#1138)
* Split components into separate files
* Use config addon to use redux
* Update tests
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 @@
optionName: 'test',
value: 'test'
};
- const failXhr = { responseText: '{}' };
+ const spySaveConfigOption = sinon.stub(ConfigAPI, 'saveConfigOption');
+ const spyDeleteConfigOption = sinon.stub(ConfigAPI, 'deleteConfigOption');
+ const dispatch = sinon.stub();
- describe('add', () => {
+ describe('addOption', () => {
+
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);
+ 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 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_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);
- });
- });
-
- 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);
-
- return Actions.addOption(node, option)
- .then(() => {
- assert.ok(spy.calledOnce);
- });
- });
-
- 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);
-
- return Actions.addOption(node, option)
- .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);
+ 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('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_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('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);
-
- return Actions.saveOption(node, option)
- .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);
+ 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 succeeds', () => {
- const stub = sinon.stub(Backbone.Model.prototype, 'destroy');
- const spy = sinon.spy(FauxtonAPI, 'addNotification');
- const promise = FauxtonAPI.Deferred();
- promise.resolve();
- 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);
- });
- });
-
- 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);
-
- return Actions.deleteOption(node, option)
- .then(() => {
- assert.ok(spy.calledOnce);
- });
- });
-
- 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);
-
- return Actions.deleteOption(node, option)
- .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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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('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.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 @@
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 @@
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 @@
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 @@
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';
+import * as ConfigAPI from './api';
-export default {
- fetchAndEditConfig (node) {
- FauxtonAPI.dispatch({ type: ActionTypes.LOADING_CONFIG });
+export const fetchAndEditConfig = (node) => (dispatch) => {
+ 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 });
-
- var modelAttrs = options;
- modelAttrs.node = node;
- var optionModel = new Resources.OptionModel(modelAttrs);
-
- return optionModel.save()
- .then(
- () => this.optionSaveSuccess(options),
- xhr => this.optionSaveFailure(options, JSON.parse(xhr.responseText).reason)
- );
- },
-
- optionSaveSuccess (options) {
- FauxtonAPI.dispatch({ type: ActionTypes.OPTION_SAVE_SUCCESS, options });
- FauxtonAPI.addNotification({
- msg: `Option ${options.optionName} saved`,
- type: 'success'
+ ConfigAPI.fetchConfig(node).then(res => {
+ dispatch({
+ type: ActionTypes.EDIT_CONFIG,
+ options: {
+ sections: res.sections,
+ node
+ }
});
- },
-
- optionSaveFailure (options, error) {
- FauxtonAPI.dispatch({ type: ActionTypes.OPTION_SAVE_FAILURE, options });
+ }).catch(err => {
FauxtonAPI.addNotification({
- msg: `Option save failed: ${error}`,
- type: 'error'
+ msg: 'Failed to load the configuration. ' + err.message,
+ type: 'error',
+ clear: true
});
- },
-
- addOption (node, options) {
- FauxtonAPI.dispatch({ type: ActionTypes.ADDING_OPTION });
-
- var modelAttrs = options;
- modelAttrs.node = node;
- var optionModel = new Resources.OptionModel(modelAttrs);
-
- return optionModel.save()
- .then(
- () => this.optionAddSuccess(options),
- xhr => this.optionAddFailure(options, JSON.parse(xhr.responseText).reason)
- );
- },
-
- optionAddSuccess (options) {
- FauxtonAPI.dispatch({ type: ActionTypes.OPTION_ADD_SUCCESS, options });
- FauxtonAPI.addNotification({
- msg: `Option ${options.optionName} added`,
- type: 'success'
+ dispatch({
+ type: ActionTypes.EDIT_CONFIG,
+ options: {
+ sections: [],
+ node
+ }
});
- },
+ });
+};
- optionAddFailure (options, error) {
- FauxtonAPI.dispatch({ type: ActionTypes.OPTION_ADD_FAILURE, options });
- FauxtonAPI.addNotification({
- msg: `Option add failed: ${error}`,
- type: 'error'
- });
- },
+export const editOption = (options) => (dispatch) => {
+ dispatch({ type: ActionTypes.EDIT_OPTION, options });
+};
- deleteOption (node, options) {
- FauxtonAPI.dispatch({ type: ActionTypes.DELETING_OPTION, options });
+export const cancelEdit = (options) => (dispatch) => {
+ dispatch({ type: ActionTypes.CANCEL_EDIT, options });
+};
- var modelAttrs = options;
- modelAttrs.node = node;
- var optionModel = new Resources.OptionModel(modelAttrs);
+export const saveOption = (node, options) => (dispatch) => {
+ dispatch({ type: ActionTypes.SAVING_OPTION, options });
- return optionModel.destroy()
- .then(() => this.optionDeleteSuccess(options))
- .catch((err) => this.optionDeleteFailure(options, err.message));
- },
+ const { sectionName, optionName, value } = options;
+ return ConfigAPI.saveConfigOption(node, sectionName, optionName, value).then(
+ () => optionSaveSuccess(options, dispatch)
+ ).catch(
+ (err) => optionSaveFailure(options, err.message, dispatch)
+ );
+};
- optionDeleteSuccess (options) {
- FauxtonAPI.dispatch({ type: ActionTypes.OPTION_DELETE_SUCCESS, options });
- FauxtonAPI.addNotification({
- msg: `Option ${options.optionName} deleted`,
- type: 'success'
- });
- },
+const optionSaveSuccess = (options, dispatch) => {
+ dispatch({ type: ActionTypes.OPTION_SAVE_SUCCESS, options });
+ FauxtonAPI.addNotification({
+ msg: `Option ${options.optionName} saved`,
+ type: 'success'
+ });
+};
- optionDeleteFailure (options, error) {
- FauxtonAPI.dispatch({ type: ActionTypes.OPTION_DELETE_FAILURE, options });
- FauxtonAPI.addNotification({
- msg: `Option delete failed: ${error}`,
- type: 'error'
- });
- }
+const optionSaveFailure = (options, error, dispatch) => {
+ dispatch({ type: ActionTypes.OPTION_SAVE_FAILURE, options });
+ FauxtonAPI.addNotification({
+ msg: `Option save failed: ${error}`,
+ type: 'error'
+ });
+};
+
+export const addOption = (node, options) => (dispatch) => {
+ dispatch({ type: ActionTypes.ADDING_OPTION });
+
+ const { sectionName, optionName, value } = options;
+ return ConfigAPI.saveConfigOption(node, sectionName, optionName, value).then(
+ () => optionAddSuccess(options, dispatch)
+ ).catch(
+ (err) => optionAddFailure(options, err.message, dispatch)
+ );
+};
+
+const optionAddSuccess = (options, dispatch) => {
+ dispatch({ type: ActionTypes.OPTION_ADD_SUCCESS, options });
+ FauxtonAPI.addNotification({
+ msg: `Option ${options.optionName} added`,
+ type: 'success'
+ });
+};
+
+const optionAddFailure = (options, error, dispatch) => {
+ dispatch({ type: ActionTypes.OPTION_ADD_FAILURE, options });
+ FauxtonAPI.addNotification({
+ msg: `Option add failed: ${error}`,
+ type: 'error'
+ });
+};
+
+export const deleteOption = (node, options) => (dispatch) => {
+ dispatch({ type: ActionTypes.DELETING_OPTION, options });
+
+ const { sectionName, optionName } = options;
+ return ConfigAPI.deleteConfigOption(node, sectionName, optionName).then(
+ () => optionDeleteSuccess(options, dispatch)
+ ).catch(
+ (err) => optionDeleteFailure(options, err.message, dispatch)
+ );
+};
+
+const optionDeleteSuccess = (options, dispatch) => {
+ dispatch({ type: ActionTypes.OPTION_DELETE_SUCCESS, options });
+ FauxtonAPI.addNotification({
+ msg: `Option ${options.optionName} deleted`,
+ type: 'success'
+ });
+};
+
+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 @@
});
};
+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 ConfigLayout = ({showCors, docURL, node, endpoint, crumbs}) => {
+export const ConfigLayout = ({ showCors, docURL, node, endpoint, crumbs }) => {
const sidebarItems = [
{
title: 'Main config',
@@ -51,7 +53,7 @@
}
];
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 @@
/>
<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 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 @@
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
-};