Merge "Ports Plan creation/update/deletion to redux"
This commit is contained in:
commit
ae11d49f12
|
@ -1,3 +1,4 @@
|
|||
app.conf
|
||||
node_modules
|
||||
dist/js
|
||||
dist/css
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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('');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">×</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);
|
||||
|
|
|
@ -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: []
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
||||
<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>…</em>
|
||||
<em>Deleting <strong>{plan.name}</strong>…</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
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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()
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import { Schema } from 'normalizr';
|
||||
|
||||
export const planSchema = new Schema('plan', { idAttribute: 'name' });
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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`
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
Loading…
Reference in New Issue