Deployment Detail

* Adds DeploymentProgress, DeploymentSuccess
  and DeploymentFailure to deployment detail
* Adds StackResourcesTable component
* DeploymentProgress and DeploymentFailure components
  display StackResourcesTable

Closes-Bug: 1623977
Change-Id: I70bfaa8c0f1994226b826cdd725967b575d2165f
This commit is contained in:
Jiri Tomasek 2016-10-11 14:05:02 +02:00
parent 32da7d2381
commit e529993fc6
19 changed files with 462 additions and 180 deletions

View File

@ -1,4 +1,4 @@
import { Map } from 'immutable'; import { Map, OrderedMap } from 'immutable';
import matchers from 'jasmine-immutable-matchers'; import matchers from 'jasmine-immutable-matchers';
import { StacksState, Stack } from '../../js/immutableRecords/stacks'; import { StacksState, Stack } from '../../js/immutableRecords/stacks';
@ -68,7 +68,7 @@ describe('stacksReducer state', () => {
description: undefined, description: undefined,
id: undefined, id: undefined,
parent: undefined, parent: undefined,
resources: Map(), resources: OrderedMap(),
stack_name: 'overcloud', stack_name: 'overcloud',
stack_owner: undefined, stack_owner: undefined,
stack_status: 'CREATE_COMPLETE', stack_status: 'CREATE_COMPLETE',

View File

@ -1,7 +1,7 @@
import { Map } from 'immutable'; import { Map } from 'immutable';
import matchers from 'jasmine-immutable-matchers'; import matchers from 'jasmine-immutable-matchers';
import { getCurrentStackDeploymentProgress, import { getCurrentStackDeploymentInProgress,
getCurrentStack } from '../../js/selectors/stacks'; getCurrentStack } from '../../js/selectors/stacks';
import { CurrentPlanState } from '../../js/immutableRecords/currentPlan'; import { CurrentPlanState } from '../../js/immutableRecords/currentPlan';
import { Stack, StacksState } from '../../js/immutableRecords/stacks'; import { Stack, StacksState } from '../../js/immutableRecords/stacks';
@ -31,7 +31,7 @@ describe('stacks selectors', () => {
}); });
}); });
describe('getCurrentStackDeploymentProgress', () => { describe('getCurrentStackDeploymentInProgress', () => {
it('returns true if the current plan\'s deployment is in progress', () => { it('returns true if the current plan\'s deployment is in progress', () => {
const state = { const state = {
stacks: new StacksState({ stacks: new StacksState({
@ -44,7 +44,7 @@ describe('stacks selectors', () => {
currentPlanName: 'overcloud' currentPlanName: 'overcloud'
}) })
}; };
expect(getCurrentStackDeploymentProgress(state)).toBe(true); expect(getCurrentStackDeploymentInProgress(state)).toBe(true);
}); });
it('returns false if the current plan\'s deployment is not in progress', () => { it('returns false if the current plan\'s deployment is not in progress', () => {
@ -59,7 +59,7 @@ describe('stacks selectors', () => {
currentplanname: 'overcloud' currentplanname: 'overcloud'
}) })
}; };
expect(getCurrentStackDeploymentProgress(state)).toBe(false); expect(getCurrentStackDeploymentInProgress(state)).toBe(false);
}); });
it('returns false if the current plan does not have an associated stack', () => { it('returns false if the current plan does not have an associated stack', () => {
@ -73,7 +73,7 @@ describe('stacks selectors', () => {
currentplanname: 'overcloud' currentplanname: 'overcloud'
}) })
}; };
expect(getCurrentStackDeploymentProgress(state)).toBe(false); expect(getCurrentStackDeploymentInProgress(state)).toBe(false);
}); });
}); });
}); });

View File

@ -43,88 +43,75 @@ export default {
}; };
}, },
fetchStackPending() { fetchResourcesPending() {
return { return {
type: StacksConstants.FETCH_STACK_PENDING type: StacksConstants.FETCH_RESOURCES_PENDING
}; };
}, },
fetchStackSuccess(stack) { fetchResourcesSuccess(resources) {
return { return {
type: StacksConstants.FETCH_STACK_SUCCESS, type: StacksConstants.FETCH_RESOURCES_SUCCESS,
payload: stack payload: resources
}; };
}, },
fetchStackFailed() { fetchResourcesFailed() {
return { return {
type: StacksConstants.FETCH_STACK_FAILED type: StacksConstants.FETCH_RESOURCES_FAILED
}; };
}, },
fetchStack(stackName, stackId) { fetchResources(stackName, stackId) {
return (dispatch) => { return (dispatch) => {
dispatch(this.fetchStackPending()); dispatch(this.fetchResourcesPending());
HeatApiService.getStack(stackName, stackId).then(({ stack }) => { HeatApiService.getResources(stackName, stackId).then(({ resources }) => {
return HeatApiService.getResources(stack).then(({ resources }) => { const res = normalize(resources,
stack.resources = normalize(resources,
arrayOf(stackResourceSchema)).entities.stackResources || {}; arrayOf(stackResourceSchema)).entities.stackResources || {};
dispatch(this.fetchStackSuccess(stack)); dispatch(this.fetchResourcesSuccess(res));
});
}).catch((error) => { }).catch((error) => {
console.error('Error retrieving resources StackActions.fetchResources', error); //eslint-disable-line no-console console.error('Error retrieving resources StackActions.fetchResources', error); //eslint-disable-line no-console
let errorHandler = new HeatApiErrorHandler(error); let errorHandler = new HeatApiErrorHandler(error);
errorHandler.errors.forEach((error) => { errorHandler.errors.forEach((error) => {
dispatch(NotificationActions.notify(error)); dispatch(NotificationActions.notify(error));
}); });
dispatch(this.fetchStackFailed(error)); dispatch(this.fetchResourcesFailed(error));
}); });
}; };
}, },
fetchResourceSuccess(stack, resourceName, resource) { fetchResourceSuccess(resource) {
return { return {
type: StacksConstants.FETCH_RESOURCE_SUCCESS, type: StacksConstants.FETCH_RESOURCE_SUCCESS,
payload: { payload: resource
stack,
resourceName,
resource
}
}; };
}, },
fetchResourceFailed(stack, resourceName) { fetchResourceFailed(resourceName) {
return { return {
type: StacksConstants.FETCH_RESOURCE_FAILED, type: StacksConstants.FETCH_RESOURCE_FAILED,
payload: { payload: resourceName
stack,
resourceName
}
}; };
}, },
fetchResourcePending(stack, resourceName) { fetchResourcePending() {
return { return {
type: StacksConstants.FETCH_RESOURCE_PENDING, type: StacksConstants.FETCH_RESOURCE_PENDING
payload: {
stack,
resourceName
}
}; };
}, },
fetchResource(stack, resourceName) { fetchResource(stack, resourceName) {
return (dispatch) => { return (dispatch) => {
dispatch(this.fetchResourcePending(stack)); dispatch(this.fetchResourcePending());
HeatApiService.getResource(stack, resourceName).then((response) => { HeatApiService.getResource(stack, resourceName).then(({ resource }) => {
dispatch(this.fetchResourceSuccess(stack, resourceName, response)); dispatch(this.fetchResourceSuccess(resource));
}).catch((error) => { }).catch((error) => {
console.error('Error retrieving resource StackActions.fetchResource', error); //eslint-disable-line no-console console.error('Error retrieving resource StackActions.fetchResource', error); //eslint-disable-line no-console
let errorHandler = new HeatApiErrorHandler(error); let errorHandler = new HeatApiErrorHandler(error);
errorHandler.errors.forEach((error) => { errorHandler.errors.forEach((error) => {
dispatch(NotificationActions.notify(error)); dispatch(NotificationActions.notify(error));
}); });
dispatch(this.fetchResourceFailed(error)); dispatch(this.fetchResourceFailed(resourceName));
}); });
}; };
}, },

View File

@ -1,35 +1,15 @@
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router';
import React from 'react'; import React from 'react';
import BlankSlate from '../ui/BlankSlate'; import BlankSlate from '../ui/BlankSlate';
import InlineNotification from '../ui/InlineNotification'; import InlineNotification from '../ui/InlineNotification';
import Loader from '../ui/Loader'; import Loader from '../ui/Loader';
import { ModalPanelBackdrop,
ModalPanel,
ModalPanelHeader,
ModalPanelBody,
ModalPanelFooter } from '../ui/ModalPanel';
const DeploymentConfirmation = ({ allValidationsSuccessful, const DeploymentConfirmation = ({ allValidationsSuccessful,
currentPlan, currentPlan,
deployPlan, deployPlan,
environmentSummary }) => { environmentSummary }) => {
return ( return (
<div>
<ModalPanelBackdrop />
<ModalPanel>
<ModalPanelHeader>
<Link to="/deployment-plan"
type="button"
className="close">
<span aria-hidden="true" className="pficon pficon-close"/>
</Link>
<h2 className="modal-title">
Deploy Plan {currentPlan.name}
</h2>
</ModalPanelHeader>
<ModalPanelBody>
<div className="col-sm-12 deployment-summary"> <div className="col-sm-12 deployment-summary">
<BlankSlate iconClass="fa fa-cloud-upload" <BlankSlate iconClass="fa fa-cloud-upload"
title={`Deploy Plan ${currentPlan.name}`}> title={`Deploy Plan ${currentPlan.name}`}>
@ -44,16 +24,6 @@ const DeploymentConfirmation = ({ allValidationsSuccessful,
isRequestingPlanDeploy={currentPlan.isRequestingPlanDeploy}/> isRequestingPlanDeploy={currentPlan.isRequestingPlanDeploy}/>
</BlankSlate> </BlankSlate>
</div> </div>
</ModalPanelBody>
<ModalPanelFooter>
<Link to="/deployment-plan"
type="button"
className="btn btn-default">
Cancel
</Link>
</ModalPanelFooter>
</ModalPanel>
</div>
); );
}; };
DeploymentConfirmation.propTypes = { DeploymentConfirmation.propTypes = {

View File

@ -1,52 +1,119 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router';
import React from 'react'; import React from 'react';
import DeploymentConfirmation from './DeploymentConfirmation';
import { getCurrentPlan } from '../../selectors/plans';
import { getCurrentStack } from '../../selectors/stacks';
import { getEnvironmentConfigurationSummary } from '../../selectors/environmentConfiguration';
import { allPreDeploymentValidationsSuccessful } from '../../selectors/validations'; import { allPreDeploymentValidationsSuccessful } from '../../selectors/validations';
import DeploymentConfirmation from './DeploymentConfirmation';
import DeploymentProgress from './DeploymentProgress';
import DeploymentSuccess from './DeploymentSuccess';
import DeploymentFailure from './DeploymentFailure';
import { getCurrentPlan } from '../../selectors/plans';
import { getCurrentStack,
getCurrentStackDeploymentProgress } from '../../selectors/stacks';
import { getEnvironmentConfigurationSummary } from '../../selectors/environmentConfiguration';
import Loader from '../ui/Loader';
import { ModalPanelBackdrop,
ModalPanel,
ModalPanelHeader,
ModalPanelBody,
ModalPanelFooter } from '../ui/ModalPanel';
import PlanActions from '../../actions/PlansActions'; import PlanActions from '../../actions/PlansActions';
import { stackStates } from '../../constants/StacksConstants'; import { stackStates } from '../../constants/StacksConstants';
import StacksActions from '../../actions/StacksActions';
const DeploymentDetail = ({ currentPlan, class DeploymentDetail extends React.Component {
renderStatus() {
const { allPreDeploymentValidationsSuccessful,
currentPlan,
currentStack, currentStack,
currentStackDeploymentProgress,
currentStackResources,
currentStackResourcesLoaded,
deployPlan, deployPlan,
environmentConfigurationSummary, environmentConfigurationSummary,
allPreDeploymentValidationsSuccessful }) => { fetchStackResources,
stacksLoaded } = this.props;
if (!currentStack || currentStack.stack_status === stackStates.DELETE_COMPLETE) { if (!currentStack || currentStack.stack_status === stackStates.DELETE_COMPLETE) {
return ( return (
<Loader loaded={stacksLoaded}
content="Loading Stacks..."
height={40}>
<DeploymentConfirmation <DeploymentConfirmation
allValidationsSuccessful={allPreDeploymentValidationsSuccessful} allValidationsSuccessful={allPreDeploymentValidationsSuccessful}
currentPlan={currentPlan} currentPlan={currentPlan}
deployPlan={deployPlan} deployPlan={deployPlan}
environmentSummary={environmentConfigurationSummary}/> environmentSummary={environmentConfigurationSummary}/>
</Loader>
); );
} else if (currentStack.stack_status.match(/PROGRESS/)) { } else if (currentStack.stack_status.match(/PROGRESS/)) {
return ( return (
// TODO(jtomasek): render component DeploymentProgress <DeploymentProgress stack={currentStack}
null stackResources={currentStackResources}
deploymentProgress={currentStackDeploymentProgress}
stackResourcesLoaded={currentStackResourcesLoaded}
fetchStackResources={fetchStackResources} />
); );
} else if (currentStack.stack_status.match(/COMPLETE/)) { } else if (currentStack.stack_status.match(/COMPLETE/)) {
return ( return (
// TODO(jtomasek): render component DeploymentSuccess <DeploymentSuccess stack={currentStack}
null stackResources={currentStackResources}
stackResourcesLoaded={currentStackResourcesLoaded}/>
); );
} else { } else {
return ( return (
// TODO(jtomasek): render component DeploymentFailure <DeploymentFailure fetchStackResources={fetchStackResources}
null stack={currentStack}
stackResources={currentStackResources}
stackResourcesLoaded={currentStackResourcesLoaded}
planName={currentPlan.name}/>
); );
} }
}; }
render() {
return (
<div>
<ModalPanelBackdrop />
<ModalPanel>
<ModalPanelHeader>
<Link to="/deployment-plan"
type="button"
className="close">
<span aria-hidden="true" className="pficon pficon-close"/>
</Link>
<h2 className="modal-title">
Plan {this.props.currentPlan.name} deployment
</h2>
</ModalPanelHeader>
<ModalPanelBody>
{this.renderStatus()}
</ModalPanelBody>
<ModalPanelFooter>
<Link to="/deployment-plan"
type="button"
className="btn btn-default">
Close
</Link>
</ModalPanelFooter>
</ModalPanel>
</div>
);
}
}
DeploymentDetail.propTypes = { DeploymentDetail.propTypes = {
allPreDeploymentValidationsSuccessful: React.PropTypes.bool.isRequired, allPreDeploymentValidationsSuccessful: React.PropTypes.bool.isRequired,
currentPlan: ImmutablePropTypes.record.isRequired, currentPlan: ImmutablePropTypes.record.isRequired,
currentStack: ImmutablePropTypes.record, currentStack: ImmutablePropTypes.record,
currentStackDeploymentProgress: React.PropTypes.number.isRequired,
currentStackResources: ImmutablePropTypes.map,
currentStackResourcesLoaded: React.PropTypes.bool.isRequired,
deployPlan: React.PropTypes.func.isRequired, deployPlan: React.PropTypes.func.isRequired,
environmentConfigurationSummary: React.PropTypes.string environmentConfigurationSummary: React.PropTypes.string,
fetchStackResources: React.PropTypes.func.isRequired,
stacksLoaded: React.PropTypes.bool.isRequired
}; };
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
@ -54,13 +121,19 @@ const mapStateToProps = (state) => {
allPreDeploymentValidationsSuccessful: allPreDeploymentValidationsSuccessful(state), allPreDeploymentValidationsSuccessful: allPreDeploymentValidationsSuccessful(state),
currentPlan: getCurrentPlan(state), currentPlan: getCurrentPlan(state),
currentStack: getCurrentStack(state), currentStack: getCurrentStack(state),
environmentConfigurationSummary: getEnvironmentConfigurationSummary(state) currentStackDeploymentProgress: getCurrentStackDeploymentProgress(state),
currentStackResources: state.stacks.resources,
currentStackResourcesLoaded: state.stacks.resourcesLoaded,
environmentConfigurationSummary: getEnvironmentConfigurationSummary(state),
stacksLoaded: state.stacks.isLoaded
}; };
}; };
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {
return { return {
deployPlan: planName => dispatch(PlanActions.deployPlan(planName)) deployPlan: planName => dispatch(PlanActions.deployPlan(planName)),
fetchStackResources: (stack) =>
dispatch(StacksActions.fetchResources(stack.stack_name, stack.id))
}; };
}; };

View File

@ -0,0 +1,34 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import React from 'react';
import { deploymentStatusMessages } from '../../constants/StacksConstants';
import InlineNotification from '../ui/InlineNotification';
import StackResourcesTable from './StackResourcesTable';
export default class DeploymentSuccess extends React.Component {
componentDidMount() {
this.props.fetchStackResources(this.props.stack);
}
render() {
return (
<div className="col-sm-12">
<InlineNotification type="error"
title={deploymentStatusMessages[this.props.stack.stack_status]}>
<p>{this.props.stack.stack_status_reason}</p>
</InlineNotification>
<h2>Resources</h2>
<StackResourcesTable isFetchingResources={!this.props.stackResourcesLoaded}
resources={this.props.stackResources.reverse()}/>
</div>
);
}
}
DeploymentSuccess.propTypes = {
fetchStackResources: React.PropTypes.func.isRequired,
planName: React.PropTypes.string.isRequired,
stack: ImmutablePropTypes.record.isRequired,
stackResources: ImmutablePropTypes.map.isRequired,
stackResourcesLoaded: React.PropTypes.bool.isRequired
};

View File

@ -0,0 +1,50 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import React from 'react';
import { deploymentStatusMessages as statusMessages,
stackStates } from '../../constants/StacksConstants';
import Loader from '../ui/Loader';
import ProgressBar from '../ui/ProgressBar';
import StackResourcesTable from './StackResourcesTable';
export default class DeploymentProgress extends React.Component {
componentDidMount() {
this.props.fetchStackResources(this.props.stack);
}
renderProgressBar() {
return (
this.props.stack.stack_status === stackStates.CREATE_IN_PROGRESS ? (
<ProgressBar value={this.props.deploymentProgress}
label={this.props.deploymentProgress + '%'}
labelPosition="topRight"/>
) : null
);
}
render() {
const statusMessage = (
<strong>{statusMessages[this.props.stack.stack_status]}</strong>
);
return (
<div className="col-sm-12">
<div className="progress-description">
<Loader loaded={false} content={statusMessage} inline/>
</div>
{this.renderProgressBar()}
<h2>Resources</h2>
<StackResourcesTable isFetchingResources={!this.props.stackResourcesLoaded}
resources={this.props.stackResources.reverse()}/>
</div>
);
}
}
DeploymentProgress.propTypes = {
deploymentProgress: React.PropTypes.number.isRequired,
fetchStackResources: React.PropTypes.func.isRequired,
stack: ImmutablePropTypes.record.isRequired,
stackResources: ImmutablePropTypes.map.isRequired,
stackResourcesLoaded: React.PropTypes.bool.isRequired
};

View File

@ -0,0 +1,41 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import React from 'react';
import Loader from '../ui/Loader';
import { deploymentStatusMessages } from '../../constants/StacksConstants';
import InlineNotification from '../ui/InlineNotification';
export default class DeploymentSuccess extends React.Component {
render() {
const ip = this.props.stackResources.getIn([
'PublicVirtualIP', 'attributes', 'ip_address'
]);
const password = this.props.stack.getIn([
'environment', 'parameter_defaults', 'AdminPassword'
]);
return (
<div className="col-sm-12">
<InlineNotification type="success"
title={deploymentStatusMessages[this.props.stack.stack_status]}>
<p>{this.props.stack.stack_status_reason}</p>
</InlineNotification>
<h4>Overcloud information:</h4>
<Loader loaded={this.props.stackResourcesLoaded}
content="Loading overcloud information...">
<ul>
<li>Overcloud IP address: <a href={`http://${ip}`}>http://{ip}</a></li>
<li>Password: {password}</li>
</ul>
</Loader>
</div>
);
}
}
DeploymentSuccess.propTypes = {
stack: ImmutablePropTypes.record.isRequired,
stackResources: ImmutablePropTypes.map.isRequired,
stackResourcesLoaded: React.PropTypes.bool.isRequired
};

View File

@ -0,0 +1,78 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DataTable from '../ui/tables/DataTable';
import { DataTableDateFieldCell,
DataTableDataFieldCell,
DataTableHeaderCell } from '../ui/tables/DataTableCells';
import DataTableColumn from '../ui/tables/DataTableColumn';
import Loader from '../ui/Loader';
export default class StackResourcesTable extends React.Component {
constructor() {
super();
this.state = {
filterString: ''
};
}
renderNoResourcesFound() {
return (
<tr>
<td className="no-results" colSpan="10">
<Loader loaded={!this.props.isFetchingResources}
height={40}
content="Loading Resources...">
<p className="text-center">There are no Resources available</p>
</Loader>
</td>
</tr>
);
}
onFilter(filterString) {
this.setState({
filterString: filterString
});
}
_filterData(filterString, data) {
let dataKeys = ['resource_name', 'resource_status'];
return filterString ? data.filter((row) => {
let result = dataKeys.filter((dataKey) => {
return row[dataKey].toLowerCase().includes(filterString.toLowerCase());
});
return result.length > 0;
}) : data;
}
render() {
let filteredData = this._filterData(this.state.filterString,
this.props.resources.toList().toJS());
return (
<DataTable {...this.props}
data={this.props.resources.toList().toJS()}
rowsCount={filteredData.length}
noRowsRenderer={this.renderNoResourcesFound.bind(this)}
onFilter={this.onFilter.bind(this)}
filterString={this.state.filterString}>
<DataTableColumn
key="resource_name"
header={<DataTableHeaderCell key="resource_name">Name</DataTableHeaderCell>}
cell={<DataTableDataFieldCell data={filteredData} field="resource_name"/>}/>
<DataTableColumn
key="resource_status"
header={<DataTableHeaderCell key="resource_status">Status</DataTableHeaderCell>}
cell={<DataTableDataFieldCell data={filteredData} field="resource_status"/>}/>
<DataTableColumn
key="updated_time"
header={<DataTableHeaderCell key="updated_time">Updated Time</DataTableHeaderCell>}
cell={<DataTableDateFieldCell data={filteredData} field="updated_time"/>}/>
</DataTable>
);
}
}
StackResourcesTable.propTypes = {
isFetchingResources: React.PropTypes.bool.isRequired,
resources: ImmutablePropTypes.map.isRequired
};

View File

@ -8,9 +8,10 @@ import Link from '../ui/Link';
import Loader from '../ui/Loader'; import Loader from '../ui/Loader';
import { stackStates } from '../../constants/StacksConstants'; import { stackStates } from '../../constants/StacksConstants';
export const DeployStep = ({ currentPlan, currentStack, deployPlan, fetchStack, fetchStackResource, export const DeployStep = ({ currentPlan, currentStack, currentStackResources,
fetchStackEnvironment, runPostDeploymentValidations, currentStackResourcesLoaded, currentStackDeploymentProgress,
stacksLoaded }) => { deployPlan, fetchStackResource, fetchStackEnvironment,
runPostDeploymentValidations, stacksLoaded }) => {
if (!currentStack || currentStack.stack_status === stackStates.DELETE_COMPLETE) { if (!currentStack || currentStack.stack_status === stackStates.DELETE_COMPLETE) {
return ( return (
<Loader loaded={stacksLoaded}> <Loader loaded={stacksLoaded}>
@ -29,11 +30,13 @@ export const DeployStep = ({ currentPlan, currentStack, deployPlan, fetchStack,
} else if (currentStack.stack_status.match(/PROGRESS/)) { } else if (currentStack.stack_status.match(/PROGRESS/)) {
return ( return (
<DeploymentProgress stack={currentStack} <DeploymentProgress stack={currentStack}
fetchStack={fetchStack} /> deploymentProgress={currentStackDeploymentProgress}/>
); );
} else if (currentStack.stack_status.match(/COMPLETE/)) { } else if (currentStack.stack_status.match(/COMPLETE/)) {
return ( return (
<DeploymentSuccess stack={currentStack} <DeploymentSuccess stack={currentStack}
stackResources={currentStackResources}
stackResourcesLoaded={currentStackResourcesLoaded}
fetchStackResource={fetchStackResource} fetchStackResource={fetchStackResource}
fetchStackEnvironment={fetchStackEnvironment} fetchStackEnvironment={fetchStackEnvironment}
runPostDeploymentValidations={runPostDeploymentValidations}/> runPostDeploymentValidations={runPostDeploymentValidations}/>
@ -48,8 +51,10 @@ export const DeployStep = ({ currentPlan, currentStack, deployPlan, fetchStack,
DeployStep.propTypes = { DeployStep.propTypes = {
currentPlan: ImmutablePropTypes.record.isRequired, currentPlan: ImmutablePropTypes.record.isRequired,
currentStack: ImmutablePropTypes.record, currentStack: ImmutablePropTypes.record,
currentStackDeploymentProgress: React.PropTypes.number.isRequired,
currentStackResources: ImmutablePropTypes.map,
currentStackResourcesLoaded: React.PropTypes.bool.isRequired,
deployPlan: React.PropTypes.func.isRequired, deployPlan: React.PropTypes.func.isRequired,
fetchStack: React.PropTypes.func.isRequired,
fetchStackEnvironment: React.PropTypes.func.isRequired, fetchStackEnvironment: React.PropTypes.func.isRequired,
fetchStackResource: React.PropTypes.func.isRequired, fetchStackResource: React.PropTypes.func.isRequired,
runPostDeploymentValidations: React.PropTypes.func.isRequired, runPostDeploymentValidations: React.PropTypes.func.isRequired,

View File

@ -4,7 +4,8 @@ import React from 'react';
import { getAllPlansButCurrent } from '../../selectors/plans'; import { getAllPlansButCurrent } from '../../selectors/plans';
import { getCurrentStack, import { getCurrentStack,
getCurrentStackDeploymentProgress } from '../../selectors/stacks'; getCurrentStackDeploymentProgress,
getCurrentStackDeploymentInProgress } from '../../selectors/stacks';
import { getAvailableNodes, getUnassignedAvailableNodes } from '../../selectors/nodes'; import { getAvailableNodes, getUnassignedAvailableNodes } from '../../selectors/nodes';
import { getEnvironmentConfigurationSummary } from '../../selectors/environmentConfiguration'; import { getEnvironmentConfigurationSummary } from '../../selectors/environmentConfiguration';
import { getCurrentPlan } from '../../selectors/plans'; import { getCurrentPlan } from '../../selectors/plans';
@ -41,7 +42,10 @@ class DeploymentPlan extends React.Component {
if (currentStack) { if (currentStack) {
if (currentStack.stack_status.match(/PROGRESS/)) { if (currentStack.stack_status.match(/PROGRESS/)) {
clearTimeout(this.stackProgressTimeout); clearTimeout(this.stackProgressTimeout);
this.stackProgressTimeout = setTimeout(() => this.props.fetchStack(currentStack), 20000); this.stackProgressTimeout = setTimeout(() => {
this.props.fetchStacks();
this.props.fetchStackResources();
}, 20000);
} }
} }
} }
@ -69,11 +73,11 @@ class DeploymentPlan extends React.Component {
</div> </div>
<ol className="deployment-step-list"> <ol className="deployment-step-list">
<DeploymentPlanStep title="Prepare Hardware" <DeploymentPlanStep title="Prepare Hardware"
disabled={this.props.currentStackDeploymentProgress}> disabled={this.props.currentStackDeploymentInProgress}>
<HardwareStep /> <HardwareStep />
</DeploymentPlanStep> </DeploymentPlanStep>
<DeploymentPlanStep title="Specify Deployment Configuration" <DeploymentPlanStep title="Specify Deployment Configuration"
disabled={this.props.currentStackDeploymentProgress}> disabled={this.props.currentStackDeploymentInProgress}>
<ConfigurePlanStep <ConfigurePlanStep
fetchEnvironmentConfiguration={this.props.fetchEnvironmentConfiguration} fetchEnvironmentConfiguration={this.props.fetchEnvironmentConfiguration}
summary={this.props.environmentConfigurationSummary} summary={this.props.environmentConfigurationSummary}
@ -82,7 +86,7 @@ class DeploymentPlan extends React.Component {
loaded={this.props.environmentConfigurationLoaded}/> loaded={this.props.environmentConfigurationLoaded}/>
</DeploymentPlanStep> </DeploymentPlanStep>
<DeploymentPlanStep title="Configure Roles and Assign Nodes" <DeploymentPlanStep title="Configure Roles and Assign Nodes"
disabled={this.props.currentStackDeploymentProgress}> disabled={this.props.currentStackDeploymentInProgress}>
<RolesStep availableNodes={this.props.availableNodes} <RolesStep availableNodes={this.props.availableNodes}
fetchNodes={this.props.fetchNodes} fetchNodes={this.props.fetchNodes}
fetchRoles={this.props.fetchRoles} fetchRoles={this.props.fetchRoles}
@ -96,8 +100,10 @@ class DeploymentPlan extends React.Component {
<DeployStep <DeployStep
currentPlan={this.props.currentPlan} currentPlan={this.props.currentPlan}
currentStack={this.props.currentStack} currentStack={this.props.currentStack}
currentStackResources={this.props.currentStackResources}
currentStackResourcesLoaded={this.props.currentStackResourcesLoaded}
currentStackDeploymentProgress={this.props.currentStackDeploymentProgress}
deployPlan={this.props.deployPlan} deployPlan={this.props.deployPlan}
fetchStack={this.props.fetchStack}
fetchStackEnvironment={this.props.fetchStackEnvironment} fetchStackEnvironment={this.props.fetchStackEnvironment}
fetchStackResource={this.props.fetchStackResource} fetchStackResource={this.props.fetchStackResource}
runPostDeploymentValidations={ runPostDeploymentValidations={
@ -123,16 +129,19 @@ DeploymentPlan.propTypes = {
choosePlan: React.PropTypes.func, choosePlan: React.PropTypes.func,
currentPlan: ImmutablePropTypes.record, currentPlan: ImmutablePropTypes.record,
currentStack: ImmutablePropTypes.record, currentStack: ImmutablePropTypes.record,
currentStackDeploymentProgress: React.PropTypes.bool, currentStackDeploymentInProgress: React.PropTypes.bool,
currentStackDeploymentProgress: React.PropTypes.number.isRequired,
currentStackResources: ImmutablePropTypes.map,
currentStackResourcesLoaded: React.PropTypes.bool.isRequired,
deployPlan: React.PropTypes.func, deployPlan: React.PropTypes.func,
environmentConfigurationLoaded: React.PropTypes.bool, environmentConfigurationLoaded: React.PropTypes.bool,
environmentConfigurationSummary: React.PropTypes.string, environmentConfigurationSummary: React.PropTypes.string,
fetchEnvironmentConfiguration: React.PropTypes.func, fetchEnvironmentConfiguration: React.PropTypes.func,
fetchNodes: React.PropTypes.func, fetchNodes: React.PropTypes.func,
fetchRoles: React.PropTypes.func, fetchRoles: React.PropTypes.func,
fetchStack: React.PropTypes.func.isRequired,
fetchStackEnvironment: React.PropTypes.func, fetchStackEnvironment: React.PropTypes.func,
fetchStackResource: React.PropTypes.func, fetchStackResource: React.PropTypes.func,
fetchStackResources: React.PropTypes.func.isRequired,
fetchStacks: React.PropTypes.func, fetchStacks: React.PropTypes.func,
hasPlans: React.PropTypes.bool, hasPlans: React.PropTypes.bool,
inactivePlans: ImmutablePropTypes.map, inactivePlans: ImmutablePropTypes.map,
@ -152,6 +161,9 @@ export function mapStateToProps(state) {
return { return {
currentPlan: getCurrentPlan(state), currentPlan: getCurrentPlan(state),
currentStack: getCurrentStack(state), currentStack: getCurrentStack(state),
currentStackResources: state.stacks.resources,
currentStackResourcesLoaded: state.stacks.resourcesLoaded,
currentStackDeploymentInProgress: getCurrentStackDeploymentInProgress(state),
currentStackDeploymentProgress: getCurrentStackDeploymentProgress(state), currentStackDeploymentProgress: getCurrentStackDeploymentProgress(state),
environmentConfigurationLoaded: state.environmentConfiguration.loaded, environmentConfigurationLoaded: state.environmentConfiguration.loaded,
environmentConfigurationSummary: getEnvironmentConfigurationSummary(state), environmentConfigurationSummary: getEnvironmentConfigurationSummary(state),
@ -178,7 +190,8 @@ function mapDispatchToProps(dispatch) {
}, },
fetchNodes: () => dispatch(NodesActions.fetchNodes()), fetchNodes: () => dispatch(NodesActions.fetchNodes()),
fetchRoles: () => dispatch(RolesActions.fetchRoles()), fetchRoles: () => dispatch(RolesActions.fetchRoles()),
fetchStack: (stack) => dispatch(StacksActions.fetchStack(stack.stack_name, stack.id)), fetchStackResources: (stack) =>
dispatch(StacksActions.fetchResources(stack.stack_name, stack.id)),
fetchStackResource: (stack, resourceName) => fetchStackResource: (stack, resourceName) =>
dispatch(StacksActions.fetchResource(stack, resourceName)), dispatch(StacksActions.fetchResource(stack, resourceName)),
fetchStacks: () => dispatch(StacksActions.fetchStacks()), fetchStacks: () => dispatch(StacksActions.fetchStacks()),

View File

@ -1,5 +1,6 @@
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import React from 'react'; import React from 'react';
import { Link } from 'react-router';
import { deploymentStatusMessages as statusMessages, import { deploymentStatusMessages as statusMessages,
stackStates } from '../../constants/StacksConstants'; stackStates } from '../../constants/StacksConstants';
@ -7,23 +8,11 @@ import Loader from '../ui/Loader';
import ProgressBar from '../ui/ProgressBar'; import ProgressBar from '../ui/ProgressBar';
export default class DeploymentProgress extends React.Component { export default class DeploymentProgress extends React.Component {
calculateProgress() {
let allResources = this.props.stack.resources.size;
if(allResources > 0) {
let completeResources = this.props.stack.resources.filter(r => {
return r.resource_status === 'CREATE_COMPLETE';
}).size;
return Math.ceil(completeResources / allResources * 100);
}
return 0;
}
renderProgressBar() { renderProgressBar() {
const progress = this.calculateProgress();
return ( return (
this.props.stack.stack_status === stackStates.CREATE_IN_PROGRESS ? ( this.props.stack.stack_status === stackStates.CREATE_IN_PROGRESS ? (
<ProgressBar value={progress} <ProgressBar value={this.props.deploymentProgress}
label={progress + '%'} label={this.props.deploymentProgress + '%'}
labelPosition="topRight"/> labelPosition="topRight"/>
) : null ) : null
); );
@ -36,6 +25,11 @@ export default class DeploymentProgress extends React.Component {
return ( return (
<div> <div>
<p>
Deployment is currently in progress. <Link to="/deployment-plan/deployment-detail">
View detailed information
</Link>
</p>
<div className="progress-description"> <div className="progress-description">
<Loader loaded={false} content={statusMessage} inline/> <Loader loaded={false} content={statusMessage} inline/>
</div> </div>
@ -46,6 +40,6 @@ export default class DeploymentProgress extends React.Component {
} }
DeploymentProgress.propTypes = { DeploymentProgress.propTypes = {
fetchStack: React.PropTypes.func.isRequired, deploymentProgress: React.PropTypes.number.isRequired,
stack: ImmutablePropTypes.record.isRequired stack: ImmutablePropTypes.record.isRequired
}; };

View File

@ -13,8 +13,7 @@ export default class DeploymentSuccess extends React.Component {
} }
render() { render() {
const loaded = !this.props.stack.resources.isEmpty(); const ip = this.props.stackResources.getIn([
const ip = this.props.stack.resources.getIn([
'PublicVirtualIP', 'attributes', 'ip_address' 'PublicVirtualIP', 'attributes', 'ip_address'
]); ]);
@ -31,7 +30,8 @@ export default class DeploymentSuccess extends React.Component {
<p>{this.props.stack.stack_status_reason}</p> <p>{this.props.stack.stack_status_reason}</p>
</InlineNotification> </InlineNotification>
<h4>Overcloud information:</h4> <h4>Overcloud information:</h4>
<Loader loaded={loaded} content="Loading overcloud information..."> <Loader loaded={this.props.stackResourcesLoaded}
content="Loading overcloud information...">
<ul> <ul>
<li>Overcloud IP address: <a href={`http://${ip}`}>http://{ip}</a></li> <li>Overcloud IP address: <a href={`http://${ip}`}>http://{ip}</a></li>
<li>Password: {password}</li> <li>Password: {password}</li>
@ -46,5 +46,7 @@ DeploymentSuccess.propTypes = {
fetchStackEnvironment: React.PropTypes.func.isRequired, fetchStackEnvironment: React.PropTypes.func.isRequired,
fetchStackResource: React.PropTypes.func.isRequired, fetchStackResource: React.PropTypes.func.isRequired,
runPostDeploymentValidations: React.PropTypes.func.isRequired, runPostDeploymentValidations: React.PropTypes.func.isRequired,
stack: ImmutablePropTypes.record.isRequired stack: ImmutablePropTypes.record.isRequired,
stackResources: ImmutablePropTypes.map.isRequired,
stackResourcesLoaded: React.PropTypes.bool.isRequired
}; };

View File

@ -55,6 +55,22 @@ DataTableDataFieldCell.propTypes = {
rowIndex: React.PropTypes.number rowIndex: React.PropTypes.number
}; };
export const DataTableDateFieldCell = (props) => {
//TODO(jtomasek): Update this component to parse date and format it using React Intl's
// FormatedDate
const value = _.result(props.data[props.rowIndex], props.field);
return (
<DataTableCell {...props}>
{value}
</DataTableCell>
);
};
DataTableDateFieldCell.propTypes = {
data: React.PropTypes.array.isRequired,
field: React.PropTypes.string.isRequired,
rowIndex: React.PropTypes.number
};
export class DataTableCheckBoxCell extends React.Component { export class DataTableCheckBoxCell extends React.Component {
render() { render() {
let value = _.result(this.props.data[this.props.rowIndex], this.props.field); let value = _.result(this.props.data[this.props.rowIndex], this.props.field);

View File

@ -7,9 +7,9 @@ export default keyMirror({
FETCH_RESOURCE_SUCCESS: null, FETCH_RESOURCE_SUCCESS: null,
FETCH_RESOURCE_PENDING: null, FETCH_RESOURCE_PENDING: null,
FETCH_RESOURCE_FAILED: null, FETCH_RESOURCE_FAILED: null,
FETCH_STACK_PENDING: null, FETCH_RESOURCES_PENDING: null,
FETCH_STACK_SUCCESS: null, FETCH_RESOURCES_SUCCESS: null,
FETCH_STACK_FAILED: null, FETCH_RESOURCES_FAILED: null,
FETCH_STACKS_PENDING: null, FETCH_STACKS_PENDING: null,
FETCH_STACKS_SUCCESS: null, FETCH_STACKS_SUCCESS: null,
FETCH_STACKS_FAILED: null FETCH_STACKS_FAILED: null

View File

@ -1,8 +1,11 @@
import { List, Map, Record } from 'immutable'; import { List, Map, OrderedMap, Record } from 'immutable';
export const StacksState = Record({ export const StacksState = Record({
isLoaded: false, isLoaded: false,
isFetching: false, isFetching: false,
isFetchingResources: false,
resourcesLoaded: false,
resources: OrderedMap(),
stacks: Map() stacks: Map()
}); });
@ -13,7 +16,6 @@ export const Stack = Record({
environment: Map(), environment: Map(),
id: undefined, id: undefined,
parent: undefined, parent: undefined,
resources: Map(),
stack_name: undefined, stack_name: undefined,
stack_owner: undefined, stack_owner: undefined,
stack_status: undefined, stack_status: undefined,

View File

@ -2,6 +2,7 @@ import { fromJS, Map } from 'immutable';
import { Stack, StackResource, StacksState } from '../immutableRecords/stacks'; import { Stack, StackResource, StacksState } from '../immutableRecords/stacks';
import StacksConstants from '../constants/StacksConstants'; import StacksConstants from '../constants/StacksConstants';
import PlansConstants from '../constants/PlansConstants';
const initialState = new StacksState; const initialState = new StacksState;
@ -23,19 +24,19 @@ export default function stacksReducer(state = initialState, action) {
.set('isFetching', false) .set('isFetching', false)
.set('stacks', Map()); .set('stacks', Map());
case StacksConstants.FETCH_STACK_PENDING: case StacksConstants.FETCH_RESOURCES_PENDING:
return state.set('isFetching', true); return state.set('isFetchingResources', true);
case StacksConstants.FETCH_STACK_SUCCESS: { case StacksConstants.FETCH_RESOURCES_SUCCESS: {
const stack = new Stack(fromJS(action.payload)) return state.set('isFetchingResources', false)
.update('resources', resources => resources .set('resourcesLoaded', true)
.map(resource => new StackResource(resource))); .set('resources',
return state.set('isFetching', false) fromJS(action.payload).map(resource => new StackResource(resource))
.mergeDeepIn(['stacks', action.payload.stack_name], stack); .sortBy(resource => resource.updated_time));
} }
case StacksConstants.FETCH_STACK_FAILED: case StacksConstants.FETCH_RESOURCES_FAILED:
return state.set('isFetching', false); return state.set('isFetchingResources', false);
case StacksConstants.FETCH_ENVIRONMENT_SUCCESS: case StacksConstants.FETCH_ENVIRONMENT_SUCCESS:
return state.setIn( return state.setIn(
@ -43,13 +44,12 @@ export default function stacksReducer(state = initialState, action) {
fromJS(action.payload.environment)); fromJS(action.payload.environment));
case StacksConstants.FETCH_RESOURCE_SUCCESS: case StacksConstants.FETCH_RESOURCE_SUCCESS:
if (state.stacks.get(action.payload.stack.stack_name)) { return state.set('resourcesLoaded', true)
return state.setIn( .setIn(['resources', action.payload.resource_name],
['stacks', action.payload.stack.stack_name, 'resources', action.payload.resourceName], new StackResource(fromJS(action.payload)));
new StackResource(fromJS(action.payload.resource.resource))
); case PlansConstants.PLAN_CHOSEN:
} return initialState;
return state;
default: default:
return state; return state;

View File

@ -3,7 +3,8 @@ import { createSelector } from 'reselect';
import { Stack } from '../immutableRecords/stacks'; import { Stack } from '../immutableRecords/stacks';
import { currentPlanNameSelector } from './plans'; import { currentPlanNameSelector } from './plans';
const stacksSelector = state => state.stacks.get('stacks'); const stacksSelector = state => state.stacks.stacks;
const stackResourcesSelector = state => state.stacks.resources;
/** /**
* Returns the stack associated with currentPlanName * Returns the stack associated with currentPlanName
@ -17,9 +18,25 @@ export const getCurrentStack = createSelector(
* Returns a flag for the deployment progress of the current plan * Returns a flag for the deployment progress of the current plan
* (true if the plan is currently being deployed, false it not). * (true if the plan is currently being deployed, false it not).
*/ */
export const getCurrentStackDeploymentProgress = createSelector( export const getCurrentStackDeploymentInProgress = createSelector(
[stacksSelector, currentPlanNameSelector], [stacksSelector, currentPlanNameSelector],
(stacks, currentPlanName) => { (stacks, currentPlanName) => {
return stacks.get(currentPlanName, new Stack()).stack_status === 'CREATE_IN_PROGRESS'; return stacks.get(currentPlanName, new Stack()).stack_status === 'CREATE_IN_PROGRESS';
} }
); );
/**
* Returns calculated percentage of deployment progress
*/
export const getCurrentStackDeploymentProgress = createSelector(
[stackResourcesSelector], (resources) => {
let allResources = resources.size;
if(allResources > 0) {
let completeResources = resources.filter(r => {
return r.resource_status === 'CREATE_COMPLETE';
}).size;
return Math.ceil(completeResources / allResources * 100);
}
return 0;
}
);

View File

@ -32,8 +32,8 @@ class HeatApiService {
return this.defaultRequest(`/stacks/${stackName}/${stackId}`); return this.defaultRequest(`/stacks/${stackName}/${stackId}`);
} }
getResources(stack) { getResources(stackName, stackId) {
return this.defaultRequest(`/stacks/${stack.stack_name}/${stack.id}/resources`); return this.defaultRequest(`/stacks/${stackName}/${stackId}/resources`);
} }
getResource(stack, resourceName) { getResource(stack, resourceName) {