Merge "Ports Plan creation/update/deletion to redux"

This commit is contained in:
Jiri Tomasek 2016-03-07 15:09:45 +01:00 committed by Gerrit Code Review
commit ae11d49f12
20 changed files with 642 additions and 525 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
app.conf
node_modules
dist/js
dist/css

View File

@ -3,6 +3,7 @@ import when from 'when';
import IronicApiService from '../../js/services/IronicApiService';
import NodesActions from '../../js/actions/NodesActions';
import NodesConstants from '../../js/constants/NodesConstants';
import * as utils from '../../js/services/utils';
const mockGetNodesResponse = [
{ uuid: 1 },
@ -62,9 +63,10 @@ let createResolvingPromise = (data) => {
};
};
// TODO(flfuchs): Fix this test (missing appState login property error after rebase).
xdescribe('Asynchronous Nodes Actions', () => {
describe('Asynchronous Nodes Actions', () => {
beforeEach(done => {
spyOn(utils, 'getAuthTokenId').and.returnValue('mock-auth-token');
spyOn(utils, 'getServiceUrl').and.returnValue('mock-url');
spyOn(NodesActions, 'requestNodes');
spyOn(NodesActions, 'receiveNodes');
// Mock the service call.

View File

@ -1,6 +1,10 @@
import { List, Map } from 'immutable';
import { normalize, arrayOf } from 'normalizr';
import when from 'when';
import * as utils from '../../js/services/utils';
import { Plan } from '../../js/immutableRecords/plans';
import { planSchema } from '../../js/normalizrSchemas/plans';
import TripleOApiService from '../../js/services/TripleOApiService';
const PlansActions = require('../../js/actions/PlansActions');
@ -18,14 +22,6 @@ describe('plansReducer default state', () => {
expect(state.get('isFetchingPlans')).toBe(false);
});
it('`isFetchingPlan` is false', () => {
expect(state.get('isFetchingPlan')).toBe(false);
});
it('`isDeletingPlan` is false', () => {
expect(state.get('isDeletingPlan')).toBe(false);
});
it('`conflict` is undefined', () => {
expect(state.get('conflict')).not.toBeDefined();
});
@ -34,10 +30,6 @@ describe('plansReducer default state', () => {
expect(state.get('currentPlanName')).not.toBeDefined();
});
it('`planData` is defined', () => {
expect(state.get('planData').size).toBeDefined();
});
it('`all` is empty', () => {
expect(state.get('all').size).toEqual(0);
});
@ -85,10 +77,10 @@ describe('plansReducer default state', () => {
isFetchingPlans: true,
all: List()
}),
PlansActions.receivePlans([
PlansActions.receivePlans(normalize([
{ name: 'overcloud' },
{ name: 'another-cloud' }
])
], arrayOf(planSchema)))
);
});
@ -96,40 +88,71 @@ describe('plansReducer default state', () => {
expect(state.get('isFetchingPlans')).toBe(false);
});
it('sets `all` to a list of plan names', () => {
expect(state.get('all')).toEqual(List.of(...['another-cloud', 'overcloud']));
});
});
describe('REQUEST_PLAN', () => {
let state;
beforeEach(() => {
state = plansReducer(
Map({ isFetchingPlan: false }),
PlansActions.requestPlan());
});
it('sets `isFetchingPlan` to true', () => {
expect(state.get('isFetchingPlan')).toBe(true);
it('sets `all` to a list of Plan records', () => {
expect(state.get('all').size).toEqual(2);
state.get('all').forEach(item => {
expect(item instanceof Plan).toBe(true);
});
});
});
describe('RECEIVE_PLAN', () => {
let state;
let state, plan;
beforeEach(() => {
state = plansReducer(
undefined,
PlansActions.receivePlan({ name: 'overcloud' }));
Map({
all: Map({
'some-cloud': new Plan({name: 'some-cloud' }),
'overcloud': new Plan({name: 'overcloud' })
})
}),
PlansActions.receivePlan({
name: 'overcloud',
files: {
'capabilities_map.yaml': {
contents: 'foo',
meta: { 'file-type': 'capabilities-map' }
},
'foo.yaml': {
contents: 'bar'
}
}
})
);
plan = state.getIn(['all', 'overcloud']);
});
it('sets `isFetchingPlan` to false', () => {
expect(state.get('isFetchingPlan')).toBe(false);
it('updates the plan records `files` attributes', () => {
expect(plan.get('files').size).toEqual(2);
});
});
describe('Plan deletion', () => {
let state = Map({
all: Map({
overcloud: new Plan({ name: 'overcloud' }),
somecloud: new Plan({ name: 'somecloud' })
})
});
let newState;
it('DELETING_PLAN sets `transition` in plan Record to `deleting`', () => {
newState = plansReducer(
state,
PlansActions.deletingPlan('somecloud')
);
let plan = newState.getIn(['all', 'somecloud']);
expect(plan.get('transition')).toBe('deleting');
});
it('updates `planData`', () => {
expect(state.get('planData').get('overcloud').name).toBe('overcloud');
it('PLAN_DELETED sets `transition` in plan Record to false', () => {
newState = plansReducer(
newState,
PlansActions.planDeleted('somecloud')
);
let plan = newState.getIn(['all', 'somecloud']);
expect(plan.get('transition')).toBe(false);
});
});
});
@ -143,6 +166,91 @@ let createResolvingPromise = (data) => {
};
describe('PlansActions', () => {
beforeEach(() => {
spyOn(utils, 'getAuthTokenId').and.returnValue('mock-auth-token');
});
describe('updatePlan', () => {
beforeEach(done => {
spyOn(PlansActions, 'updatingPlan');
spyOn(PlansActions, 'planUpdated');
spyOn(PlansActions, 'fetchPlans');
// Mock the service call.
spyOn(TripleOApiService, 'updatePlan').and.callFake(createResolvingPromise());
// Call the action creator and the resulting action.
// In this case, dispatch and getState are just empty placeHolders.
PlansActions.updatePlan('somecloud', {})(() => {}, () => {});
// Call done with a minimal timeout.
setTimeout(() => { done(); }, 1);
});
it('dispatches updatingPlan', () => {
expect(PlansActions.updatingPlan).toHaveBeenCalledWith('somecloud');
});
it('dispatches planUpdated', () => {
expect(PlansActions.planUpdated).toHaveBeenCalledWith('somecloud');
});
it('dispatches fetchPlans', () => {
expect(PlansActions.fetchPlans).toHaveBeenCalled();
});
});
describe('createPlan', () => {
beforeEach(done => {
spyOn(PlansActions, 'creatingPlan');
spyOn(PlansActions, 'planCreated');
spyOn(PlansActions, 'fetchPlans');
// Mock the service call.
spyOn(TripleOApiService, 'createPlan').and.callFake(createResolvingPromise());
// Call the action creator and the resulting action.
// In this case, dispatch and getState are just empty placeHolders.
PlansActions.createPlan('somecloud', {})(() => {}, () => {});
// Call done with a minimal timeout.
setTimeout(() => { done(); }, 1);
});
it('dispatches creatingPlan', () => {
expect(PlansActions.creatingPlan).toHaveBeenCalled();
});
it('dispatches planCreated', () => {
expect(PlansActions.planCreated).toHaveBeenCalled();
});
it('dispatches fetchPlans', () => {
expect(PlansActions.fetchPlans).toHaveBeenCalled();
});
});
describe('deletePlans', () => {
beforeEach(done => {
spyOn(PlansActions, 'deletingPlan');
spyOn(PlansActions, 'planDeleted');
spyOn(PlansActions, 'fetchPlans');
// Mock the service call.
spyOn(TripleOApiService, 'deletePlan').and.callFake(createResolvingPromise());
// Call the action creator and the resulting action.
// In this case, dispatch and getState are just empty placeHolders.
PlansActions.deletePlan('somecloud')(() => {}, () => {});
// Call done with a minimal timeout.
setTimeout(() => { done(); }, 1);
});
it('dispatches deletingPlan', () => {
expect(PlansActions.deletingPlan).toHaveBeenCalledWith('somecloud');
});
it('dispatches planDeleted', () => {
expect(PlansActions.planDeleted).toHaveBeenCalledWith('somecloud');
});
it('dispatches fetchPlans', () => {
expect(PlansActions.fetchPlans).toHaveBeenCalled();
});
});
describe('fetchPlans', () => {
let apiResponse = {
plans: [
@ -168,7 +276,16 @@ describe('PlansActions', () => {
});
it('dispatches receivePlans', () => {
expect(PlansActions.receivePlans).toHaveBeenCalledWith(apiResponse.plans);
let expected = {
result: ['overcloud', 'another-cloud'],
entities: {
plan: {
'overcloud': { name: 'overcloud' },
'another-cloud': { name: 'another-cloud' }
}
}
};
expect(PlansActions.receivePlans).toHaveBeenCalledWith(expected);
});
});

View File

@ -1,18 +1,93 @@
import { Map } from 'immutable';
import React from 'react';
import TestUtils from 'react-addons-test-utils';
import ListPlans from '../../../js/components/plan/ListPlans';
import FileList from '../../../js/components/plan/FileList';
import { PlanFile } from '../../../js/immutableRecords/plans';
import store from '../../../js/store';
describe('ListPlans component', () => {
let output;
beforeEach(() => {
let shallowRenderer = TestUtils.createRenderer();
shallowRenderer.render(<ListPlans />);
shallowRenderer.render(<ListPlans store={store} />);
output = shallowRenderer.getRenderOutput();
});
xit('renders a table of plan names', () => {
it('renders a table of plan names', () => {
expect(output.type.name).toEqual('ListPlans');
});
});
let getTableRows = (planFiles, selectedFiles) => {
let result;
let shallowRenderer = TestUtils.createRenderer();
shallowRenderer.render(<FileList planFiles={planFiles}
selectedFiles={selectedFiles} />);
result = shallowRenderer.getRenderOutput();
return result.props.children[1].props.children.props.children;
};
describe('FileList component', () => {
it('renders a list of plan files, ordered alphabetically', () => {
let tableRows = getTableRows(
Map({
'foo.yaml': new PlanFile({ name: 'foo.yaml' }),
'bar.yaml': new PlanFile({ name: 'bar.yaml' })
}),
[]
);
expect(tableRows[0].key).toBe('bar.yaml');
expect(tableRows[1].key).toBe('foo.yaml');
});
it('renders a list of selected files, ordered alphabetically', () => {
let tableRows = getTableRows(
Map(),
[
{ name: 'foo.yaml', content: 'foo' },
{ name: 'bar.yaml', content: 'bar' }
]
);
expect(tableRows[0].key).toBe('bar.yaml');
expect(tableRows[1].key).toBe('foo.yaml');
});
it('merges a list of selected files and planfiles', () => {
let tableRows = getTableRows(
Map({
'foobar.yaml': new PlanFile({ name: 'foobar.yaml' }),
'bar.yaml': new PlanFile({ name: 'bar.yaml' })
}),
[
{ name: 'foo.yaml', content: 'foo' },
{ name: 'bar.yaml', content: 'bar' }
]
);
expect(tableRows[0].key).toBe('bar.yaml');
expect(tableRows[1].key).toBe('foo.yaml');
expect(tableRows[2].key).toBe('foobar.yaml');
});
it('adds classes and sorts files based on differences in selected files and planfiles', () => {
let tableRows = getTableRows(
Map({
'foo.yaml': new PlanFile({ name: 'foo.yaml', contents: 'foo' }),
'bar.yaml': new PlanFile({ name: 'bar.yaml', contents: 'bar' })
}),
[
{ name: 'foo.yaml', content: 'foo' },
{ name: 'bar.yaml', content: 'changed' },
{ name: 'foobar.yaml', content: 'foobar' }
]
);
expect(tableRows[0].key).toBe('bar.yaml');
expect(tableRows[0].props.children.props.className).toBe('changed-plan-file');
expect(tableRows[1].key).toBe('foobar.yaml');
expect(tableRows[1].props.children.props.className).toBe('new-plan-file');
expect(tableRows[2].key).toBe('foo.yaml');
expect(tableRows[2].props.children.props.className).toBe('');
});
});

View File

@ -1,79 +0,0 @@
import PlansConstants from '../../js/constants/PlansConstants';
import PlansStore from '../../js/stores/PlansStore';
xdescribe('PlansStore', () => {
xit('should call onListPlans on LIST_PLANS action', () => {
spyOn(PlansStore, 'onListPlans').and.callThrough();
let payload = { actionType: PlansConstants.LIST_PLANS,
plans: [ 'p1', 'p2'] };
PlansStore._registerToActions(payload);
expect(PlansStore.onListPlans).toHaveBeenCalledWith(payload.plans);
expect(PlansStore.state.plans).toEqual(payload.plans);
});
describe('.onListPlans(plans)', () => {
let newPlans = [{name: 'plan-1'}, {name: 'plan-2'}];
it('updates state.plans', () => {
PlansStore.onListPlans(newPlans);
expect(PlansStore.state.plans).toEqual([{name: 'plan-1'}, {name: 'plan-2'}]);
});
});
xdescribe('.onGetPlan', () => {
beforeEach(() => {
PlansStore.state = {
currentPlanName: undefined,
plans: []
};
});
it('updates the state', () => {
PlansStore.onGetPlan('new-plan');
expect(PlansStore.state.currentPlanName).toEqual('new-plan');
});
});
});
xdescribe('PlansStore plan detection', () => {
beforeEach(() => {
spyOn(window.localStorage, 'setItem');
PlansStore.state = {
currentPlanName: undefined,
plans: [],
conflict: undefined
};
});
it('saves the new plan name in localStorage', () => {
PlansStore.state = {
currentPlanName: undefined,
plans: []
};
PlansStore.onGetPlan('some-plan');
expect(window.localStorage.setItem).toHaveBeenCalledWith('currentPlanName', 'some-plan');
});
it('activates the plan in localStorage if there is none in state', () => {
spyOn(window.localStorage, 'getItem').and.returnValue('plan-2');
PlansStore.onListPlans([{name: 'plan-1'}, {name: 'plan-2'}]);
expect(PlansStore.state.currentPlanName).toBe('plan-2');
expect(window.localStorage.setItem).toHaveBeenCalledWith('currentPlanName', 'plan-2');
});
it('activates the first existing plan if plan in state does not exist.', () => {
PlansStore.state.currentPlanName = 'no-match';
PlansStore.onListPlans([{name: 'plan-1'}, {name: 'plan-2'}]);
expect(PlansStore.state.currentPlanName).toBe('plan-1');
expect(PlansStore.state.conflict).toBe('no-match');
expect(window.localStorage.setItem).toHaveBeenCalledWith('currentPlanName', 'plan-1');
});
it('does not try to choose a plan if there is none', () => {
spyOn(window.localStorage, 'getItem').and.returnValue(null);
PlansStore.onListPlans([]);
expect(PlansStore.state.currentPlanName).not.toBeDefined();
expect(PlansStore.state.conflict).not.toBeDefined();
expect(window.localStorage.setItem).not.toHaveBeenCalled();
});
});

View File

@ -1,8 +1,9 @@
import * as _ from 'lodash';
import { normalize, arrayOf } from 'normalizr';
import AppDispatcher from '../dispatchers/AppDispatcher.js';
import history from '../history';
import NotificationActions from '../actions/NotificationActions';
import PlansConstants from '../constants/PlansConstants';
import { planSchema } from '../normalizrSchemas/plans';
import TripleOApiService from '../services/TripleOApiService';
import TripleOApiErrorHandler from '../services/TripleOApiErrorHandler';
@ -32,13 +33,13 @@ export default {
detectPlan(plans) {
return (dispatch, getState) => {
let planNames = _.map(plans, plan => plan.name).sort();
let state = getState();
let plans = state.plans.get('all').map(plan => plan.get('name'));
let conflict;
let currentPlanName = state.plans.get('currentPlanName');
let previousPlan = currentPlanName || getStoredPlan();
// No plans present.
if(planNames.length < 1 ) {
if(plans.size < 1 ) {
if(!previousPlan) {
currentPlanName = undefined;
}
@ -46,12 +47,12 @@ export default {
// Plans present.
// No previously chosen plan.
else if(!previousPlan) {
currentPlanName = planNames[0];
currentPlanName = plans.first();
}
// Previously chosen plan doesn't exist any more.
else if(!_.includes(planNames, previousPlan)) {
else if(!plans.includes(previousPlan)) {
conflict = previousPlan;
currentPlanName = planNames[0];
currentPlanName = plans.first();
}
// No plan in state, but in localStorage
else if(!currentPlanName && previousPlan) {
@ -66,20 +67,13 @@ export default {
return dispatch => {
dispatch(this.requestPlans());
TripleOApiService.getPlans().then(response => {
/*
* TODO(flfuchs) remove when delete plans is implemented as redux action
*/
AppDispatcher.dispatch({
actionType: PlansConstants.LIST_PLANS,
plans: response.plans
});
dispatch(this.receivePlans(response.plans));
dispatch(this.detectPlan(response.plans));
let normalizedData = normalize(response.plans, arrayOf(planSchema));
dispatch(this.receivePlans(normalizedData));
dispatch(this.detectPlan(normalizedData));
}).catch(error => {
console.error('Error retrieving plans PlansActions.fetchPlanslist', error); //eslint-disable-line no-console
console.error('Error retrieving plans PlansActions.fetchPlans', error); //eslint-disable-line no-console
dispatch(this.receivePlans([]));
dispatch(this.detectPlan([]));
dispatch(this.detectPlan(normalize([], arrayOf(planSchema))));
let errorHandler = new TripleOApiErrorHandler(error);
errorHandler.errors.forEach((error) => {
dispatch(NotificationActions.notify(error));
@ -108,7 +102,6 @@ export default {
dispatch(this.receivePlan(response.plan));
}).catch(error => {
console.error('Error retrieving plan PlansActions.fetchPlan', error); //eslint-disable-line no-console
dispatch(this.receivePlan({}));
let errorHandler = new TripleOApiErrorHandler(error);
errorHandler.errors.forEach((error) => {
dispatch(NotificationActions.notify(error));
@ -136,32 +129,119 @@ export default {
};
},
/*
* TODO(flfuchs) remove when delete plans is implemented as redux action
*/
listPlans() {
TripleOApiService.getPlans().then(res => {
}).catch(error => {
let errorHandler = new TripleOApiErrorHandler(error);
errorHandler.errors.forEach((error) => {
NotificationActions.notify(error);
updatingPlan(planName) {
return {
type: PlansConstants.UPDATING_PLAN,
payload: planName
};
},
planUpdated(planName) {
return {
type: PlansConstants.PLAN_UPDATED,
payload: planName
};
},
updatePlan(planName, planFiles) {
return dispatch => {
dispatch(this.updatingPlan(planName));
TripleOApiService.updatePlan(
planName,
planFiles
).then(result => {
dispatch(this.planUpdated(planName));
dispatch(this.fetchPlans());
history.pushState(null, '/plans/list');
dispatch(NotificationActions.notify({
title: 'Plan Updated',
message: `The plan ${planName} was successfully updated.`,
type: 'success'
}));
}).catch(error => {
console.error('Error in PlansActions.updatePlan', error); //eslint-disable-line no-console
let errorHandler = new TripleOApiErrorHandler(error);
errorHandler.errors.forEach((error) => {
dispatch(NotificationActions.notify(error));
});
});
});
};
},
creatingPlan() {
return {
type: PlansConstants.CREATING_PLAN
};
},
planCreated() {
return {
type: PlansConstants.PLAN_CREATED
};
},
createPlan(planName, planFiles) {
return dispatch => {
dispatch(this.creatingPlan());
TripleOApiService.createPlan(
planName,
planFiles
).then(result => {
dispatch(this.planCreated(planName));
dispatch(this.fetchPlans());
history.pushState(null, '/plans/list');
dispatch(NotificationActions.notify({
title: 'Plan Created',
message: `The plan ${planName} was successfully created.`,
type: 'success'
}));
}).catch(error => {
console.error('Error in PlansActions.createPlan', error); //eslint-disable-line no-console
let errorHandler = new TripleOApiErrorHandler(error);
errorHandler.errors.forEach((error) => {
dispatch(NotificationActions.notify(error));
});
});
};
},
deletingPlan(planName) {
AppDispatcher.dispatch({
actionType: PlansConstants.DELETING_PLAN,
planName: planName
});
return {
type: PlansConstants.DELETING_PLAN,
payload: planName
};
},
planDeleted(planName) {
AppDispatcher.dispatch({
actionType: PlansConstants.PLAN_DELETED,
planName: planName
});
return {
type: PlansConstants.PLAN_DELETED,
payload: planName
};
},
deletePlan(planName) {
return dispatch => {
dispatch(this.deletingPlan(planName));
history.pushState(null, '/plans/list');
TripleOApiService.deletePlan(planName).then(response => {
dispatch(this.planDeleted(planName));
dispatch(this.fetchPlans());
dispatch(NotificationActions.notify({
title: 'Plan Deleted',
message: `The plan ${planName} was successfully deleted.`,
type: 'success'
}));
}).catch(error => {
console.error('Error retrieving plan TripleOApiService.deletePlan', error); //eslint-disable-line no-console
dispatch(this.planDeleted(planName));
let errorHandler = new TripleOApiErrorHandler(error);
errorHandler.errors.forEach((error) => {
dispatch(NotificationActions.notify(error));
});
});
};
}
};
function storePlan(name) {

View File

@ -1,47 +1,19 @@
import { connect } from 'react-redux';
import React from 'react';
import { Link } from 'react-router';
import NotificationActions from '../../actions/NotificationActions';
import PlansActions from '../../actions/PlansActions';
import TripleOApiErrorHandler from '../../services/TripleOApiErrorHandler';
import TripleOApiService from '../../services/TripleOApiService';
export default class DeletePlan extends React.Component {
constructor() {
super();
this.onDeleteClick = this._onDeleteClick.bind(this);
}
class DeletePlan extends React.Component {
getNameFromUrl() {
let planName = this.props.params.planName || '';
return planName.replace(/[^A-Za-z0-9_-]*/g, '');
}
_onDeleteClick() {
onDeleteClick() {
let planName = this.getNameFromUrl();
if(planName) {
PlansActions.deletingPlan(planName);
this.props.history.pushState(null, 'plans/list');
TripleOApiService.deletePlan(planName).then(result => {
PlansActions.planDeleted(planName);
NotificationActions.notify({
title: 'Plan Deleted',
message: `The plan ${planName} was successfully deleted.`,
type: 'success'
});
}).catch(error => {
console.error('Error in DeletePlan._ondeleteClick', error); //eslint-disable-line no-console
PlansActions.listPlans();
let errorHandler = new TripleOApiErrorHandler(error);
errorHandler.errors.forEach((error) => {
NotificationActions.notify({
title: 'Error Deleting Plan',
message: `The plan ${planName} could not be deleted. ${error.message}`,
type: 'error'
});
});
});
this.props.deletePlan(planName);
}
}
@ -68,7 +40,7 @@ export default class DeletePlan extends React.Component {
</div>
<div className="modal-footer">
<button className="btn btn-danger"
onClick={this.onDeleteClick}
onClick={this.onDeleteClick.bind(this)}
type="submit">
Delete Plan
</button>
@ -84,6 +56,16 @@ export default class DeletePlan extends React.Component {
}
DeletePlan.propTypes = {
history: React.PropTypes.object,
deletePlan: React.PropTypes.func,
params: React.PropTypes.object
};
function mapDispatchToProps(dispatch) {
return {
deletePlan: planName => {
dispatch(PlansActions.deletePlan(planName));
}
};
}
export default connect(null, mapDispatchToProps)(DeletePlan);

View File

@ -1,168 +1,48 @@
import { connect } from 'react-redux';
import Formsy from 'formsy-react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router';
import React from 'react';
import FormErrorList from '../ui/forms/FormErrorList';
import NotificationActions from '../../actions/NotificationActions';
import PlanEditFormTabs from './PlanEditFormTabs';
import TripleOApiErrorHandler from '../../services/TripleOApiErrorHandler';
import TripleOApiService from '../../services/TripleOApiService';
import PlansActions from '../../actions/PlansActions';
export default class EditPlan extends React.Component {
class EditPlan extends React.Component {
constructor() {
super();
this.state = {
plan: {
name: undefined,
files: []
},
filesObj: {},
planName: undefined,
selectedFiles: undefined,
canSubmit: false,
formErrors: []
};
this.processPlanFiles = this._processPlanFiles.bind(this);
this.createFilesArray = this._createFilesArray.bind(this);
}
componentDidMount() {
this.state.plan.name = this.getNameFromUrl();
TripleOApiService.getPlan(this.state.plan.name).then(result => {
this.processPlanFiles(result.plan.files);
this.setState({ plan: {
name: this.state.plan.name,
files: this.createFilesArray()
}});
}).catch(error => {
console.error('Error in TripleOApiService.getPlan', error); //eslint-disable-line no-console
let errorHandler = new TripleOApiErrorHandler(error);
this.setState({
formErrors: errorHandler.errors.map((error) => {
return {
title: 'Error Retrieving Plan Data',
message: `The plan ${this.state.plan.name} could not be retrieved. ${error.message}`,
type: 'error'
};
})
});
});
this.state.planName = this.getNameFromUrl();
this.props.fetchPlan(this.state.planName);
}
_createFilesArray() {
let arr = [];
for(let key in this.state.filesObj) {
arr.push({
name: key,
content: this.state.filesObj[key].content,
info: this.state.filesObj[key].info
});
}
return arr.sort((a, b) => {
if (a.info.newFile && !b.info.newFile) {
return -1;
}
else if (b.info.newFile && !a.info.newFile) {
return 1;
}
else if (a.info.changed && !b.info.changed) {
return -1;
}
else if (b.info.changed && !a.info.changed) {
return 1;
}
else if(a.name > b.name) {
return 1;
}
else if(a.name < b.name) {
return -1;
}
else {
return 0;
}
});
}
_processPlanFiles(planFilesObj) {
for(let key in planFilesObj) {
let item = planFilesObj[key];
let obj = this.state.filesObj[key] || {};
obj.name = key;
if(!obj.info) {
obj.info = {
changed: false
};
}
obj.info.newFile = false;
// If the same file has been selected from disk:
// Are the contents the same?
if (obj.content && obj.content !== item.contents) {
obj.info.changed = true;
}
else {
obj.content = item.contents;
}
this.state.filesObj[key] = obj;
}
}
onPlanFilesChange(currentValues, isChanged) {
let files = currentValues.planFiles;
if (files && files != []) {
files.forEach(item => {
let obj = this.state.filesObj[item.name] || {};
obj.name = item.name;
if(!obj.info) {
obj.info = {
newFile: true
};
}
// If the same file has been selected from disk:
// Are the contents the same?
obj.info.changed = (obj.content && obj.content !== item.content);
obj.content = item.content;
this.state.filesObj[item.name] = obj;
});
this.setState({ plan: {
name: this.state.plan.name,
files: this.createFilesArray()
}});
onPlanFilesChange(currentValues) {
if(currentValues && currentValues.planFiles) {
this.setState({ selectedFiles: currentValues.planFiles });
}
}
onFormSubmit(form) {
let planFiles = {};
this.state.plan.files.forEach(item => {
// online upload new or changed files
if(item.info.changed || item.info.newFile) {
planFiles[item.name] = {};
planFiles[item.name].contents = item.content;
// TODO(jtomasek): user can identify capabilities-map in the list of files
// (dropdown select or sth)
if(item.name === 'capabilities_map.yaml') {
planFiles[item.name].meta = { 'file-type': 'capabilities-map' };
}
this.state.selectedFiles.map(item => {
planFiles[item.name] = {};
planFiles[item.name].contents = item.content;
// TODO(jtomasek): user can identify capabilities-map in the list of files
// (dropdown select or sth)
if(item.name.match('^capabilities[-|_]map\.yaml$')) {
planFiles[item.name].meta = { 'file-type': 'capabilities-map' };
}
});
TripleOApiService.updatePlan(this.state.plan.name, planFiles).then(result => {
this.props.history.pushState(null, 'plans/list');
NotificationActions.notify({
title: 'Plan Updated',
message: `The plan ${this.state.plan.name} was successfully updated.`,
type: 'success'
});
}).catch(error => {
console.error('Error in TripleOApiService.updatePlan', error); //eslint-disable-line no-console
let errorHandler = new TripleOApiErrorHandler(error);
this.setState({
formErrors: errorHandler.errors.map((error) => {
return {
title: 'Error Updating Plan',
message: `The plan ${this.state.plan.name} could not be updated. ${error.message}`,
type: 'error'
};
})
});
});
this.props.updatePlan(this.state.planName, planFiles);
}
onFormValid() {
@ -178,7 +58,10 @@ export default class EditPlan extends React.Component {
return planName.replace(/[^A-Za-z0-9_-]*/g, '');
}
render () {
render() {
let plan = this.props.plans.filter(plan => plan.name === this.state.planName).first();
let planFiles = plan ? plan.files : undefined;
return (
<div>
<div className="modal modal-routed in" role="dialog">
@ -197,13 +80,14 @@ export default class EditPlan extends React.Component {
className="close">
<span aria-hidden="true">&times;</span>
</Link>
<h4>Update {this.state.plan.name} Files</h4>
<h4>Update {this.state.planName} Files</h4>
</div>
<div className="modal-body">
<FormErrorList errors={this.state.formErrors}/>
<PlanEditFormTabs currentTab={this.props.location.query.tab || 'editPlan'}
planFiles={this.state.plan.files}
planName={this.state.plan.name}/>
planFiles={planFiles}
selectedFiles={this.state.selectedFiles}
planName={this.state.planName}/>
</div>
<div className="modal-footer">
<button disabled={!this.state.canSubmit}
@ -223,7 +107,29 @@ export default class EditPlan extends React.Component {
}
EditPlan.propTypes = {
fetchPlan: React.PropTypes.func,
history: React.PropTypes.object,
location: React.PropTypes.object,
params: React.PropTypes.object
params: React.PropTypes.object,
plans: ImmutablePropTypes.map,
updatePlan: React.PropTypes.func
};
function mapStateToProps(state) {
return {
plans: state.plans.get('all')
};
}
function mapDispatchToProps(dispatch) {
return {
fetchPlan: planName => {
dispatch(PlansActions.fetchPlan(planName));
},
updatePlan: (planName, files) => {
dispatch(PlansActions.updatePlan(planName, files));
}
};
}
export default connect(mapStateToProps, mapDispatchToProps)(EditPlan);

View File

@ -1,14 +1,64 @@
import ClassNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Map } from 'immutable';
import { PlanFile } from '../../immutableRecords/plans';
import React from 'react';
export default class FileList extends React.Component {
getMergedFiles(planFiles, selectedFiles) {
let files = {};
if(!planFiles.isEmpty()) {
planFiles.map(file => {
files[file.name] = file;
});
}
if(selectedFiles.length > 0) {
selectedFiles.forEach(file => {
let existing = files[file.name];
let info;
if(!existing) {
info = Map({
newFile: planFiles.isEmpty() ? false : true,
changed: false
});
}
else {
info = Map({
newFile: false,
changed: file.content !== existing.contents
});
}
files[file.name] = PlanFile({ name: file.name, info: info });
});
}
return Map(files)
.sort((fileA, fileB) => {
let [aName, aChangedOrNew] = [ fileA.name,
(fileA.getIn(['info', 'changed']) ||
fileA.getIn(['info', 'newFile'])) ];
let [bName, bChangedOrNew] = [ fileB.name,
(fileB.getIn(['info', 'changed']) ||
fileB.getIn(['info', 'newFile'])) ];
if(aChangedOrNew && !bChangedOrNew) {
return -1;
}
else if(!aChangedOrNew && bChangedOrNew) {
return 1;
}
else {
return aName > bName ? 1 : -1;
}
})
.toArray();
}
render() {
if(this.props.files.length === 0) {
if(this.props.planFiles.size === 0 && this.props.selectedFiles === 0) {
return null;
}
let files = this.props.files.map(file => {
let info = file.info || {};
let files = this.getMergedFiles(this.props.planFiles, this.props.selectedFiles).map(file => {
let info = file.info.toJS() || {};
let classes = ClassNames({
'changed-plan-file': info.changed,
'new-plan-file': info.newFile
@ -35,9 +85,11 @@ export default class FileList extends React.Component {
}
FileList.propTypes = {
files: React.PropTypes.array.isRequired
planFiles: ImmutablePropTypes.map,
selectedFiles: React.PropTypes.array
};
FileList.defaultProps = {
files: []
planFiles: Map(),
selectedFiles: []
};

View File

@ -47,22 +47,22 @@ class ListPlans extends React.Component {
}
render() {
let plans = this.props.plans.sortBy(plan => plan.name).toArray();
return (
<div>
<PageHeader>Plans</PageHeader>
<DataTable data={this.props.plans.toJS()}
rowsCount={this.props.plans.size}
<DataTable data={plans}
rowsCount={plans.length}
noRowsRenderer={this.renderNoPlans.bind(this)}
tableActions={this.renderTableActions}>
<DataTableColumn header={<DataTableHeaderCell key="name">Name</DataTableHeaderCell>}
cell={<PlanNameCell
data={this.props.plans.toJS()}
data={plans}
currentPlanName={this.props.currentPlanName}
choosePlan={this.props.choosePlan}/>}/>
<DataTableColumn header={<DataTableHeaderCell key="actions">Actions</DataTableHeaderCell>}
cell={<RowActionsCell className="actions text-right"
data={this.props.plans.toJS()}/>}/>
data={plans}/>}/>
</DataTable>
{this.props.children}
</div>
@ -75,15 +75,13 @@ ListPlans.propTypes = {
conflict: React.PropTypes.string,
currentPlanName: React.PropTypes.string,
fetchPlans: React.PropTypes.func,
planData: ImmutablePropTypes.map,
plans: ImmutablePropTypes.list
plans: ImmutablePropTypes.map
};
function mapStateToProps(state) {
return {
currentPlanName: state.plans.get('currentPlanName'),
conflict: state.plans.get('conflict'),
planData: state.plans.get('planData'),
plans: state.plans.get('all')
};
}
@ -113,12 +111,12 @@ class RowActionsCell extends React.Component {
return (
<DataTableCell {...this.props}>
<Link key="edit"
to={`/plans/${plan}/edit`}
to={`/plans/${plan.name}/edit`}
query={{tab: 'editPlan'}}
className="btn btn-xs btn-default">Edit</Link>
&nbsp;
<Link key="delete"
to={`/plans/${plan}/delete`}
to={`/plans/${plan.name}/delete`}
className="btn btn-xs btn-danger">Delete</Link>
</DataTableCell>
);
@ -126,7 +124,7 @@ class RowActionsCell extends React.Component {
}
}
RowActionsCell.propTypes = {
data: React.PropTypes.array.isRequired,
data: React.PropTypes.array,
rowIndex: React.PropTypes.number
};
@ -148,16 +146,17 @@ export class PlanNameCell extends React.Component {
render() {
let plan = this.props.data[this.props.rowIndex];
if(plan.transition) {
if(plan.transition === 'deleting') {
return (
<DataTableCell {...this.props} colSpan="2" className={plan.transition}>
<em>Deleting <strong>{plan}</strong>&hellip;</em>
<em>Deleting <strong>{plan.name}</strong>&hellip;</em>
</DataTableCell>
);
} else {
return (
<DataTableCell {...this.props}>
{this.getActiveIcon(plan)} <a href="" onClick={this.onPlanClick.bind(this)}>{plan}</a>
{this.getActiveIcon(plan.name)} <a href=""
onClick={this.onPlanClick.bind(this)}>{plan.name}</a>
</DataTableCell>
);
}
@ -166,6 +165,6 @@ export class PlanNameCell extends React.Component {
PlanNameCell.propTypes = {
choosePlan: React.PropTypes.func,
currentPlanName: React.PropTypes.string,
data: React.PropTypes.array.isRequired,
data: React.PropTypes.array,
rowIndex: React.PropTypes.number
};

View File

@ -1,20 +1,19 @@
import { connect } from 'react-redux';
import Formsy from 'formsy-react';
import { Link } from 'react-router';
import React from 'react';
import FormErrorList from '../ui/forms/FormErrorList';
import NotificationActions from '../../actions/NotificationActions';
import PlansActions from '../../actions/PlansActions';
import PlanFormTabs from './PlanFormTabs';
import TripleOApiErrorHandler from '../../services/TripleOApiErrorHandler';
import TripleOApiService from '../../services/TripleOApiService';
export default class NewPlan extends React.Component {
class NewPlan extends React.Component {
constructor() {
super();
this.state = {
files: [],
selectedFiles: undefined,
canSubmit: false,
formErrors: []
};
@ -23,43 +22,22 @@ export default class NewPlan extends React.Component {
onPlanFilesChange(currentValues, isChanged) {
let files = currentValues.planFiles;
if (files && files != []) {
this.setState({ files: files });
this.setState({ selectedFiles: currentValues.planFiles });
}
}
onFormSubmit(formData, resetForm, invalidateForm) {
let planFiles = {};
this.state.files.forEach(item => {
this.state.selectedFiles.map(item => {
planFiles[item.name] = {};
planFiles[item.name].contents = item.content;
// TODO(jtomasek): user can identify capabilities-map in the list of files
// (dropdown select or sth)
if(item.name === 'capabilities_map.yaml') {
if(item.name.match('^capabilities[-|_]map\.yaml$')) {
planFiles[item.name].meta = { 'file-type': 'capabilities-map' };
}
});
TripleOApiService.createPlan(formData.planName, planFiles).then(result => {
PlansActions.listPlans();
this.props.history.pushState(null, 'plans/list');
NotificationActions.notify({
title: 'Plan Created',
message: `The plan ${formData.planName} was successfully created.`,
type: 'success'
});
}).catch(error => {
console.error('Error in NewPlan.onFormSubmit', error); //eslint-disable-line no-console
let errorHandler = new TripleOApiErrorHandler(error);
this.setState({
formErrors: errorHandler.errors.map((error) => {
return {
title: 'Error Creating Plan',
message: `The plan ${formData.planName} could not be created. ${error.message}`,
type: 'error'
};
})
});
});
this.props.createPlan(formData.planName, planFiles);
}
onFormValid() {
@ -94,7 +72,7 @@ export default class NewPlan extends React.Component {
<div className="modal-body">
<FormErrorList errors={this.state.formErrors}/>
<PlanFormTabs currentTab={this.props.location.query.tab || 'newPlan'}
planFiles={this.state.files} />
selectedFiles={this.state.selectedFiles} />
</div>
<div className="modal-footer">
<button disabled={!this.state.canSubmit}
@ -114,6 +92,16 @@ export default class NewPlan extends React.Component {
}
}
NewPlan.propTypes = {
history: React.PropTypes.object,
createPlan: React.PropTypes.func,
location: React.PropTypes.object
};
function mapDispatchToProps(dispatch) {
return {
createPlan: (planName, files) => {
dispatch(PlansActions.createPlan(planName, files));
}
};
}
export default connect(null, mapDispatchToProps)(NewPlan);

View File

@ -1,3 +1,5 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import { List } from 'immutable';
import React from 'react';
import HorizontalStaticText from '../ui/forms/HorizontalStaticText';
@ -10,7 +12,16 @@ export default class PlanEditFormTabs extends React.Component {
return this.props.currentTab === tabName ? 'active' : '';
}
getFileCount() {
let planFiles = this.props.planFiles || List();
let selectedFiles = this.props.selectedFiles || [];
return selectedFiles.length > planFiles.size
? selectedFiles.length
: planFiles.size;
}
render() {
return (
<div>
<ul className="nav nav-tabs">
@ -18,14 +29,15 @@ export default class PlanEditFormTabs extends React.Component {
query={{tab: 'editPlan'}}>Update Plan</NavTab>
<NavTab to={`/plans/${this.props.planName}/edit`}
query={{tab: 'planFiles'}}>
Files <span className="badge">{this.props.planFiles.length}</span>
Files <span className="badge">{this.getFileCount.bind(this)()}</span>
</NavTab>
</ul>
<div className="tab-content">
<PlanFormTab active={this.setActiveTab('editPlan')}
planName={this.props.planName}/>
<PlanFilesTab active={this.setActiveTab('planFiles')}
planFiles={this.props.planFiles} />
planFiles={this.props.planFiles}
selectedFiles={this.props.selectedFiles}/>
</div>
</div>
);
@ -33,8 +45,9 @@ export default class PlanEditFormTabs extends React.Component {
}
PlanEditFormTabs.propTypes = {
currentTab: React.PropTypes.string,
planFiles: React.PropTypes.array.isRequired,
planName: React.PropTypes.string
planFiles: ImmutablePropTypes.map,
planName: React.PropTypes.string,
selectedFiles: React.PropTypes.array
};
PlanEditFormTabs.defaultProps = {
currentTtab: 'editPlan'

View File

@ -1,3 +1,4 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import React from 'react';
import FileList from './FileList';
@ -6,12 +7,14 @@ export default class PlanFilesTab extends React.Component {
render() {
return (
<div className={`tab-pane ${this.props.active}`}>
<FileList files={this.props.planFiles} />
<FileList planFiles={this.props.planFiles}
selectedFiles={this.props.selectedFiles} />
</div>
);
}
}
PlanFilesTab.propTypes = {
active: React.PropTypes.string,
planFiles: React.PropTypes.array
planFiles: ImmutablePropTypes.map,
selectedFiles: React.PropTypes.array
};

View File

@ -16,13 +16,13 @@ export default class PlanFormTabs extends React.Component {
<ul className="nav nav-tabs">
<NavTab to="/plans/new" query={{tab: 'newPlan'}}>New Plan</NavTab>
<NavTab to="/plans/new" query={{tab: 'planFiles'}}>
Files <span className="badge">{this.props.planFiles.length}</span>
Files <span className="badge">{this.props.selectedFiles.length}</span>
</NavTab>
</ul>
<div className="tab-content">
<PlanFormTab active={this.setActiveTab('newPlan')} />
<PlanFilesTab active={this.setActiveTab('planFiles')}
planFiles={this.props.planFiles} />
selectedFiles={this.props.selectedFiles} />
</div>
</div>
);
@ -30,10 +30,11 @@ export default class PlanFormTabs extends React.Component {
}
PlanFormTabs.propTypes = {
currentTab: React.PropTypes.string,
planFiles: React.PropTypes.array.isRequired
selectedFiles: React.PropTypes.array
};
PlanFormTabs.defaultProps = {
currentTtab: 'newPlan'
currentTtab: 'newPlan',
selectedFiles: []
};
class PlanFormTab extends React.Component {

View File

@ -9,9 +9,10 @@ export default keyMirror({
RECEIVE_PLAN_DELETE: null,
PLAN_CHOSEN: null,
PLAN_DETECTED: null,
GET_PLAN: null,
LIST_PLANS: null,
DELETING_PLAN: null,
PLAN_DELETED: null
PLAN_DELETED: null,
CREATING_PLAN: null,
PLAN_CREATED: null,
UPDATING_PLAN: null,
PLAN_UPDATED: null
});

View File

@ -0,0 +1,18 @@
import { Map, Record } from 'immutable';
/**
* The transition property is either false or one of the following strings:
* - `deleting`
* - `updating`
*/
export const Plan = Record({
name: '',
transition: false,
files: Map()
});
export const PlanFile = Record({
name: '',
contents: '',
info: Map()
});

View File

@ -0,0 +1,3 @@
import { Schema } from 'normalizr';
export const planSchema = new Schema('plan', { idAttribute: 'name' });

View File

@ -1,35 +1,50 @@
import { List, Map } from 'immutable';
import { Map } from 'immutable';
import PlansConstants from '../constants/PlansConstants';
import { Plan, PlanFile } from '../immutableRecords/plans';
const initialState = Map({
isFetchingPlans: false,
isFetchingPlan: false,
isDeletingPlan: false,
conflict: undefined,
currentPlanName: undefined,
planData: Map(),
all: List()
all: Map()
});
export default function plansReducer(state = initialState, action) {
switch(action.type) {
case PlansConstants.REQUEST_PLAN:
return state.set('isFetchingPlan', true);
return state;
case PlansConstants.RECEIVE_PLAN:
return state
.setIn(['planData', action.payload.name], action.payload)
.set('isFetchingPlan', false);
case PlansConstants.RECEIVE_PLAN: {
let filesMap = action.payload.files
? filesMap = Map(action.payload.files)
.map((item, key) => new PlanFile({
name: key,
contents: item.contents,
meta: item.meta
}))
: Map();
let newState = state
.updateIn(
['all', action.payload.name],
new Plan({ name: action.payload.name }),
plan => plan.set('files', filesMap));
return newState;
}
case PlansConstants.REQUEST_PLANS:
return state.set('isFetchingPlans', true);
case PlansConstants.RECEIVE_PLANS:
let planData = {};
action.payload.result.forEach(name => {
planData[name] = new Plan(action.payload.entities.plan[name]);
});
return state
.set('isFetchingPlans', false)
.set('all', List(action.payload).map(plan => plan.name).sort());
.set('all', Map(planData));
case PlansConstants.PLAN_CHOSEN:
return state.set('currentPlanName', action.payload);
@ -39,6 +54,30 @@ export default function plansReducer(state = initialState, action) {
.set('currentPlanName', action.payload.currentPlanName)
.set('conflict', action.payload.conflict);
case PlansConstants.DELETING_PLAN: {
return state.setIn(['all', action.payload, 'transition'], 'deleting');
}
case PlansConstants.PLAN_DELETED: {
return state.setIn(['all', action.payload, 'transition'], false);
}
case PlansConstants.CREATING_PLAN:
return state
.set('isCreatingPlan', true);
case PlansConstants.PLAN_CREATED:
return state
.set('isCreatingPlan', false);
case PlansConstants.UPDATING_PLAN:
return state
.set('transition', 'updating');
case PlansConstants.PLAN_UPDATED:
return state
.set('transition', false);
default:
return state;

View File

@ -2,15 +2,14 @@ import * as _ from 'lodash';
import request from 'reqwest';
import when from 'when';
// import LoginStore from '../stores/LoginStore';
import TempStorage from './TempStorage';
import { getAuthTokenId } from '../services/utils';
import { TRIPLEOAPI_URL } from '../constants/APIEndpointUrls';
class TripleOApiService {
defaultRequest(additionalAttributes) {
return _.merge({
headers: { 'X-Auth-Token': TempStorage.getItem('keystoneAuthTokenId') },
headers: { 'X-Auth-Token': getAuthTokenId() },
crossOrigin: true,
contentType: 'application/json',
type: 'json',
@ -23,9 +22,9 @@ class TripleOApiService {
* @returns {Promise} resolving with {array} of plans.
*/
getPlans() {
return when(request(this.defaultRequest(
{ url: TRIPLEOAPI_URL + '/plans' }
)));
return when(request(this.defaultRequest({
url: TRIPLEOAPI_URL + '/plans'
})));
}
/**
@ -33,9 +32,9 @@ class TripleOApiService {
* @returns plan.
*/
getPlan(planName) {
return when(request(this.defaultRequest(
{ url: `${TRIPLEOAPI_URL}/plans/${planName}` }
)));
return when(request(this.defaultRequest({
url: `${TRIPLEOAPI_URL}/plans/${planName}`
})));
}
/**
@ -43,9 +42,9 @@ class TripleOApiService {
* @returns Plan's environments mapping.
*/
getPlanEnvironments(planName) {
return when(request(this.defaultRequest(
{ url: `${TRIPLEOAPI_URL}/plans/${planName}/environments` }
)));
return when(request(this.defaultRequest({
url: `${TRIPLEOAPI_URL}/plans/${planName}/environments`
})));
}
/**
@ -53,13 +52,11 @@ class TripleOApiService {
* @returns Plan's environments mapping.
*/
updatePlanEnvironments(planName, data) {
return when(request(this.defaultRequest(
{
url: `${TRIPLEOAPI_URL}/plans/${planName}/environments`,
method: 'PATCH',
data: JSON.stringify(data)
}
)));
return when(request(this.defaultRequest({
url: `${TRIPLEOAPI_URL}/plans/${planName}/environments`,
method: 'PATCH',
data: JSON.stringify(data)
})));
}
/**
@ -67,9 +64,9 @@ class TripleOApiService {
* @returns Plan's parameters.
*/
getPlanParameters(planName) {
return when(request(this.defaultRequest(
{ url: `${TRIPLEOAPI_URL}/plans/${planName}/parameters` }
)));
return when(request(this.defaultRequest({
url: `${TRIPLEOAPI_URL}/plans/${planName}/parameters`
})));
}
/**
@ -89,9 +86,9 @@ class TripleOApiService {
* @returns Plan's resource registry.
*/
getPlanResourceTypes(planName) {
return when(request(this.defaultRequest(
{ url: `${TRIPLEOAPI_URL}/plans/${planName}/resource_types` }
)));
return when(request(this.defaultRequest({
url: `${TRIPLEOAPI_URL}/plans/${planName}/resource_types`
})));
}
/**
@ -99,9 +96,9 @@ class TripleOApiService {
* @returns Plan's roles mapping.
*/
getPlanRoles(planName) {
return when(request(this.defaultRequest(
{ url: `${TRIPLEOAPI_URL}/plans/${planName}/roles` }
)));
return when(request(this.defaultRequest({
url: `${TRIPLEOAPI_URL}/plans/${planName}/roles`
})));
}
/**
@ -109,9 +106,9 @@ class TripleOApiService {
* @returns Plan's validation results.
*/
validatePlan(planName) {
return when(request(this.defaultRequest(
{ url: `${TRIPLEOAPI_URL}/plans/${planName}/validate` }
)));
return when(request(this.defaultRequest({
url: `${TRIPLEOAPI_URL}/plans/${planName}/validate`
})));
}
/**

View File

@ -1,81 +0,0 @@
import * as _ from 'lodash';
import BaseStore from './BaseStore';
import PlansConstants from '../constants/PlansConstants';
class PlansStore extends BaseStore {
constructor() {
super();
this.subscribe(() => this._registerToActions.bind(this));
this.state = {
currentPlanName: undefined,
plans: [],
plansLoaded: false,
conflict: undefined
};
}
_registerToActions(payload) {
switch(payload.actionType) {
case PlansConstants.GET_PLAN:
this.onGetPlan(payload.planName);
break;
case PlansConstants.LIST_PLANS:
this.onListPlans(payload.plans);
break;
case PlansConstants.DELETING_PLAN:
this.onDeletingPlan(payload.planName);
break;
case PlansConstants.PLAN_DELETED:
this.onPlanDeleted(payload.planName);
break;
default:
break;
}
}
onGetPlan(planName) {
this.state.currentPlanName = planName;
if(window && window.localStorage) {
window.localStorage.setItem('currentPlanName', planName);
}
this.emitChange();
}
onListPlans(plans) {
this.state.plans = plans;
this.state.plansLoaded = true;
this.emitChange();
}
onDeletingPlan(planName) {
let index = _.findIndex(this.state.plans, 'name', planName);
this.state.plans[index].transition = 'deleting';
this.emitChange();
}
onPlanDeleted(planName) {
let index = _.findIndex(this.state.plans, 'name', planName);
this.state.plans = _.without(this.state.plans, this.state.plans[index]);
this.emitChange();
}
getState() {
return this.state;
}
getCurrentPlanName() {
return this.state.currentPlanName;
}
getPlans() {
return this.state.plans;
}
_getPreviousPlan() {
return this.state.currentPlanName || this.getStoredPlan();
}
}
export default new PlansStore();