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 { StacksState, Stack } from '../../js/immutableRecords/stacks';
@ -68,7 +68,7 @@ describe('stacksReducer state', () => {
description: undefined,
id: undefined,
parent: undefined,
resources: Map(),
resources: OrderedMap(),
stack_name: 'overcloud',
stack_owner: undefined,
stack_status: 'CREATE_COMPLETE',

View File

@ -1,7 +1,7 @@
import { Map } from 'immutable';
import matchers from 'jasmine-immutable-matchers';
import { getCurrentStackDeploymentProgress,
import { getCurrentStackDeploymentInProgress,
getCurrentStack } from '../../js/selectors/stacks';
import { CurrentPlanState } from '../../js/immutableRecords/currentPlan';
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', () => {
const state = {
stacks: new StacksState({
@ -44,7 +44,7 @@ describe('stacks selectors', () => {
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', () => {
@ -59,7 +59,7 @@ describe('stacks selectors', () => {
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', () => {
@ -73,7 +73,7 @@ describe('stacks selectors', () => {
currentplanname: 'overcloud'
})
};
expect(getCurrentStackDeploymentProgress(state)).toBe(false);
expect(getCurrentStackDeploymentInProgress(state)).toBe(false);
});
});
});

View File

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

View File

@ -1,58 +1,28 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router';
import React from 'react';
import BlankSlate from '../ui/BlankSlate';
import InlineNotification from '../ui/InlineNotification';
import Loader from '../ui/Loader';
import { ModalPanelBackdrop,
ModalPanel,
ModalPanelHeader,
ModalPanelBody,
ModalPanelFooter } from '../ui/ModalPanel';
const DeploymentConfirmation = ({ allValidationsSuccessful,
currentPlan,
deployPlan,
environmentSummary }) => {
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">
<BlankSlate iconClass="fa fa-cloud-upload"
title={`Deploy Plan ${currentPlan.name}`}>
<p><strong>Summary:</strong> {environmentSummary}</p>
<ValidationsWarning allValidationsSuccessful={allValidationsSuccessful}/>
<p>
Are you sure you want to deploy this plan?
</p>
<DeployButton
disabled={currentPlan.isRequestingPlanDeploy}
deploy={deployPlan.bind(this, currentPlan.name)}
isRequestingPlanDeploy={currentPlan.isRequestingPlanDeploy}/>
</BlankSlate>
</div>
</ModalPanelBody>
<ModalPanelFooter>
<Link to="/deployment-plan"
type="button"
className="btn btn-default">
Cancel
</Link>
</ModalPanelFooter>
</ModalPanel>
<div className="col-sm-12 deployment-summary">
<BlankSlate iconClass="fa fa-cloud-upload"
title={`Deploy Plan ${currentPlan.name}`}>
<p><strong>Summary:</strong> {environmentSummary}</p>
<ValidationsWarning allValidationsSuccessful={allValidationsSuccessful}/>
<p>
Are you sure you want to deploy this plan?
</p>
<DeployButton
disabled={currentPlan.isRequestingPlanDeploy}
deploy={deployPlan.bind(this, currentPlan.name)}
isRequestingPlanDeploy={currentPlan.isRequestingPlanDeploy}/>
</BlankSlate>
</div>
);
};

View File

@ -1,52 +1,119 @@
import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router';
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 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 { stackStates } from '../../constants/StacksConstants';
import StacksActions from '../../actions/StacksActions';
const DeploymentDetail = ({ currentPlan,
currentStack,
deployPlan,
environmentConfigurationSummary,
allPreDeploymentValidationsSuccessful }) => {
if (!currentStack || currentStack.stack_status === stackStates.DELETE_COMPLETE) {
class DeploymentDetail extends React.Component {
renderStatus() {
const { allPreDeploymentValidationsSuccessful,
currentPlan,
currentStack,
currentStackDeploymentProgress,
currentStackResources,
currentStackResourcesLoaded,
deployPlan,
environmentConfigurationSummary,
fetchStackResources,
stacksLoaded } = this.props;
if (!currentStack || currentStack.stack_status === stackStates.DELETE_COMPLETE) {
return (
<Loader loaded={stacksLoaded}
content="Loading Stacks..."
height={40}>
<DeploymentConfirmation
allValidationsSuccessful={allPreDeploymentValidationsSuccessful}
currentPlan={currentPlan}
deployPlan={deployPlan}
environmentSummary={environmentConfigurationSummary}/>
</Loader>
);
} else if (currentStack.stack_status.match(/PROGRESS/)) {
return (
<DeploymentProgress stack={currentStack}
stackResources={currentStackResources}
deploymentProgress={currentStackDeploymentProgress}
stackResourcesLoaded={currentStackResourcesLoaded}
fetchStackResources={fetchStackResources} />
);
} else if (currentStack.stack_status.match(/COMPLETE/)) {
return (
<DeploymentSuccess stack={currentStack}
stackResources={currentStackResources}
stackResourcesLoaded={currentStackResourcesLoaded}/>
);
} else {
return (
<DeploymentFailure fetchStackResources={fetchStackResources}
stack={currentStack}
stackResources={currentStackResources}
stackResourcesLoaded={currentStackResourcesLoaded}
planName={currentPlan.name}/>
);
}
}
render() {
return (
<DeploymentConfirmation
allValidationsSuccessful={allPreDeploymentValidationsSuccessful}
currentPlan={currentPlan}
deployPlan={deployPlan}
environmentSummary={environmentConfigurationSummary}/>
);
} else if (currentStack.stack_status.match(/PROGRESS/)) {
return (
// TODO(jtomasek): render component DeploymentProgress
null
);
} else if (currentStack.stack_status.match(/COMPLETE/)) {
return (
// TODO(jtomasek): render component DeploymentSuccess
null
);
} else {
return (
// TODO(jtomasek): render component DeploymentFailure
null
<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 = {
allPreDeploymentValidationsSuccessful: React.PropTypes.bool.isRequired,
currentPlan: ImmutablePropTypes.record.isRequired,
currentStack: ImmutablePropTypes.record,
currentStackDeploymentProgress: React.PropTypes.number.isRequired,
currentStackResources: ImmutablePropTypes.map,
currentStackResourcesLoaded: React.PropTypes.bool.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) => {
@ -54,13 +121,19 @@ const mapStateToProps = (state) => {
allPreDeploymentValidationsSuccessful: allPreDeploymentValidationsSuccessful(state),
currentPlan: getCurrentPlan(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) => {
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 { stackStates } from '../../constants/StacksConstants';
export const DeployStep = ({ currentPlan, currentStack, deployPlan, fetchStack, fetchStackResource,
fetchStackEnvironment, runPostDeploymentValidations,
stacksLoaded }) => {
export const DeployStep = ({ currentPlan, currentStack, currentStackResources,
currentStackResourcesLoaded, currentStackDeploymentProgress,
deployPlan, fetchStackResource, fetchStackEnvironment,
runPostDeploymentValidations, stacksLoaded }) => {
if (!currentStack || currentStack.stack_status === stackStates.DELETE_COMPLETE) {
return (
<Loader loaded={stacksLoaded}>
@ -29,11 +30,13 @@ export const DeployStep = ({ currentPlan, currentStack, deployPlan, fetchStack,
} else if (currentStack.stack_status.match(/PROGRESS/)) {
return (
<DeploymentProgress stack={currentStack}
fetchStack={fetchStack} />
deploymentProgress={currentStackDeploymentProgress}/>
);
} else if (currentStack.stack_status.match(/COMPLETE/)) {
return (
<DeploymentSuccess stack={currentStack}
stackResources={currentStackResources}
stackResourcesLoaded={currentStackResourcesLoaded}
fetchStackResource={fetchStackResource}
fetchStackEnvironment={fetchStackEnvironment}
runPostDeploymentValidations={runPostDeploymentValidations}/>
@ -48,8 +51,10 @@ export const DeployStep = ({ currentPlan, currentStack, deployPlan, fetchStack,
DeployStep.propTypes = {
currentPlan: ImmutablePropTypes.record.isRequired,
currentStack: ImmutablePropTypes.record,
currentStackDeploymentProgress: React.PropTypes.number.isRequired,
currentStackResources: ImmutablePropTypes.map,
currentStackResourcesLoaded: React.PropTypes.bool.isRequired,
deployPlan: React.PropTypes.func.isRequired,
fetchStack: React.PropTypes.func.isRequired,
fetchStackEnvironment: React.PropTypes.func.isRequired,
fetchStackResource: 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 { getCurrentStack,
getCurrentStackDeploymentProgress } from '../../selectors/stacks';
getCurrentStackDeploymentProgress,
getCurrentStackDeploymentInProgress } from '../../selectors/stacks';
import { getAvailableNodes, getUnassignedAvailableNodes } from '../../selectors/nodes';
import { getEnvironmentConfigurationSummary } from '../../selectors/environmentConfiguration';
import { getCurrentPlan } from '../../selectors/plans';
@ -41,7 +42,10 @@ class DeploymentPlan extends React.Component {
if (currentStack) {
if (currentStack.stack_status.match(/PROGRESS/)) {
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>
<ol className="deployment-step-list">
<DeploymentPlanStep title="Prepare Hardware"
disabled={this.props.currentStackDeploymentProgress}>
disabled={this.props.currentStackDeploymentInProgress}>
<HardwareStep />
</DeploymentPlanStep>
<DeploymentPlanStep title="Specify Deployment Configuration"
disabled={this.props.currentStackDeploymentProgress}>
disabled={this.props.currentStackDeploymentInProgress}>
<ConfigurePlanStep
fetchEnvironmentConfiguration={this.props.fetchEnvironmentConfiguration}
summary={this.props.environmentConfigurationSummary}
@ -82,7 +86,7 @@ class DeploymentPlan extends React.Component {
loaded={this.props.environmentConfigurationLoaded}/>
</DeploymentPlanStep>
<DeploymentPlanStep title="Configure Roles and Assign Nodes"
disabled={this.props.currentStackDeploymentProgress}>
disabled={this.props.currentStackDeploymentInProgress}>
<RolesStep availableNodes={this.props.availableNodes}
fetchNodes={this.props.fetchNodes}
fetchRoles={this.props.fetchRoles}
@ -96,8 +100,10 @@ class DeploymentPlan extends React.Component {
<DeployStep
currentPlan={this.props.currentPlan}
currentStack={this.props.currentStack}
currentStackResources={this.props.currentStackResources}
currentStackResourcesLoaded={this.props.currentStackResourcesLoaded}
currentStackDeploymentProgress={this.props.currentStackDeploymentProgress}
deployPlan={this.props.deployPlan}
fetchStack={this.props.fetchStack}
fetchStackEnvironment={this.props.fetchStackEnvironment}
fetchStackResource={this.props.fetchStackResource}
runPostDeploymentValidations={
@ -123,16 +129,19 @@ DeploymentPlan.propTypes = {
choosePlan: React.PropTypes.func,
currentPlan: 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,
environmentConfigurationLoaded: React.PropTypes.bool,
environmentConfigurationSummary: React.PropTypes.string,
fetchEnvironmentConfiguration: React.PropTypes.func,
fetchNodes: React.PropTypes.func,
fetchRoles: React.PropTypes.func,
fetchStack: React.PropTypes.func.isRequired,
fetchStackEnvironment: React.PropTypes.func,
fetchStackResource: React.PropTypes.func,
fetchStackResources: React.PropTypes.func.isRequired,
fetchStacks: React.PropTypes.func,
hasPlans: React.PropTypes.bool,
inactivePlans: ImmutablePropTypes.map,
@ -152,6 +161,9 @@ export function mapStateToProps(state) {
return {
currentPlan: getCurrentPlan(state),
currentStack: getCurrentStack(state),
currentStackResources: state.stacks.resources,
currentStackResourcesLoaded: state.stacks.resourcesLoaded,
currentStackDeploymentInProgress: getCurrentStackDeploymentInProgress(state),
currentStackDeploymentProgress: getCurrentStackDeploymentProgress(state),
environmentConfigurationLoaded: state.environmentConfiguration.loaded,
environmentConfigurationSummary: getEnvironmentConfigurationSummary(state),
@ -178,7 +190,8 @@ function mapDispatchToProps(dispatch) {
},
fetchNodes: () => dispatch(NodesActions.fetchNodes()),
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) =>
dispatch(StacksActions.fetchResource(stack, resourceName)),
fetchStacks: () => dispatch(StacksActions.fetchStacks()),

View File

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

View File

@ -13,8 +13,7 @@ export default class DeploymentSuccess extends React.Component {
}
render() {
const loaded = !this.props.stack.resources.isEmpty();
const ip = this.props.stack.resources.getIn([
const ip = this.props.stackResources.getIn([
'PublicVirtualIP', 'attributes', 'ip_address'
]);
@ -31,7 +30,8 @@ export default class DeploymentSuccess extends React.Component {
<p>{this.props.stack.stack_status_reason}</p>
</InlineNotification>
<h4>Overcloud information:</h4>
<Loader loaded={loaded} content="Loading overcloud information...">
<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>
@ -46,5 +46,7 @@ DeploymentSuccess.propTypes = {
fetchStackEnvironment: React.PropTypes.func.isRequired,
fetchStackResource: 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
};
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 {
render() {
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_PENDING: null,
FETCH_RESOURCE_FAILED: null,
FETCH_STACK_PENDING: null,
FETCH_STACK_SUCCESS: null,
FETCH_STACK_FAILED: null,
FETCH_RESOURCES_PENDING: null,
FETCH_RESOURCES_SUCCESS: null,
FETCH_RESOURCES_FAILED: null,
FETCH_STACKS_PENDING: null,
FETCH_STACKS_SUCCESS: 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({
isLoaded: false,
isFetching: false,
isFetchingResources: false,
resourcesLoaded: false,
resources: OrderedMap(),
stacks: Map()
});
@ -13,7 +16,6 @@ export const Stack = Record({
environment: Map(),
id: undefined,
parent: undefined,
resources: Map(),
stack_name: undefined,
stack_owner: undefined,
stack_status: undefined,

View File

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

View File

@ -3,7 +3,8 @@ import { createSelector } from 'reselect';
import { Stack } from '../immutableRecords/stacks';
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
@ -17,9 +18,25 @@ export const getCurrentStack = createSelector(
* Returns a flag for the deployment progress of the current plan
* (true if the plan is currently being deployed, false it not).
*/
export const getCurrentStackDeploymentProgress = createSelector(
export const getCurrentStackDeploymentInProgress = createSelector(
[stacksSelector, currentPlanNameSelector],
(stacks, currentPlanName) => {
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}`);
}
getResources(stack) {
return this.defaultRequest(`/stacks/${stack.stack_name}/${stack.id}/resources`);
getResources(stackName, stackId) {
return this.defaultRequest(`/stacks/${stackName}/${stackId}/resources`);
}
getResource(stack, resourceName) {