Run undeploy_plan workflow to delete deployment

* create separate UndeployProgress components to track
  deletion of deployment
* unify deploy/undeploy transition when issuing the workflow using UI
  state rather than custom UI deployment state (STARTING_DEPLOYMENT)

Depends-On: Ibd1da1aee8415712b9a418c6738f6ea9a2828a55
Implements: blueprint config-download-ui
Change-Id: Idec2534a583f6bea7c74bbdb8a08b9a81b7aa3f8
This commit is contained in:
Jiri Tomasek 2018-05-04 14:47:21 +02:00
parent 3691bab67d
commit e81f96469b
24 changed files with 724 additions and 275 deletions

View File

@ -0,0 +1,10 @@
---
features:
- |
Added integration of config-download deployment as a default deployment way
using TripleO-UI. Config Download feature splits deployment into 2 parts, at
first, Heat creates all the deployment data necessary via SoftwareDeployment
resources to perform the Overcloud installation and configuration. In second
part, using downloaded data from Heat, Ansible playbooks and tasks are
generated and are then used by the Undercloud to complete the configuration
of the Overcloud.

View File

@ -25,7 +25,12 @@ import {
START_DEPLOYMENT_PENDING,
START_DEPLOYMENT_SUCCESS,
DEPLOYMENT_FAILED,
DEPLOYMENT_SUCCESS
DEPLOYMENT_SUCCESS,
START_UNDEPLOY_FAILED,
START_UNDEPLOY_PENDING,
START_UNDEPLOY_SUCCESS,
UNDEPLOY_FAILED,
UNDEPLOY_SUCCESS
} from '../constants/DeploymentConstants';
import { handleErrors } from './ErrorActions';
import MistralConstants from '../constants/MistralConstants';
@ -91,9 +96,9 @@ export const startDeploymentSuccess = planName => ({
payload: planName
});
export const startDeploymentFailed = (planName, message) => ({
export const startDeploymentFailed = planName => ({
type: START_DEPLOYMENT_FAILED,
payload: { planName, message }
payload: planName
});
export const startDeployment = planName => dispatch => {
@ -143,3 +148,67 @@ export const deploymentFinished = execution => (
dispatch(deploymentSuccess(planName, message));
}
};
export const startUndeployPending = planName => ({
type: START_UNDEPLOY_PENDING,
payload: planName
});
export const startUndeploySuccess = planName => ({
type: START_UNDEPLOY_SUCCESS,
payload: planName
});
export const startUndeployFailed = planName => ({
type: START_UNDEPLOY_FAILED,
payload: planName
});
export const startUndeploy = planName => dispatch => {
dispatch(startUndeployPending(planName));
dispatch(
startWorkflow(
MistralConstants.UNDEPLOY_PLAN,
{
container: planName,
timeout: 240
},
execution => dispatch(undeployFinished(execution)),
10 * 60 * 1000
)
)
.then(execution => dispatch(startUndeploySuccess(planName)))
.catch(error => {
dispatch(
handleErrors(error, `Plan ${planName} deployment could not be deleted`)
);
dispatch(startUndeployFailed(planName));
});
};
export const undeploySuccess = (planName, message) => ({
type: UNDEPLOY_SUCCESS,
payload: { planName, message }
});
export const undeployFailed = (planName, message) => ({
type: UNDEPLOY_FAILED,
payload: { planName, message }
});
export const undeployFinished = execution => (
dispatch,
getState,
{ getIntl }
) => {
const {
input: { container: planName },
output: { message },
state
} = execution;
if (state === 'ERROR') {
dispatch(undeployFailed(planName, message));
} else {
dispatch(undeploySuccess(planName, message));
}
};

View File

@ -174,41 +174,5 @@ export default {
dispatch(this.fetchEnvironmentFailed(stack));
});
};
},
deleteStackSuccess(stackName) {
return {
type: StacksConstants.DELETE_STACK_SUCCESS,
payload: stackName
};
},
deleteStackFailed() {
return {
type: StacksConstants.DELETE_STACK_FAILED
};
},
deleteStackPending() {
return {
type: StacksConstants.DELETE_STACK_PENDING
};
},
/**
* Starts a delete request for a stack.
*/
deleteStack(stack) {
return dispatch => {
dispatch(this.deleteStackPending());
dispatch(HeatApiService.deleteStack(stack.stack_name, stack.id))
.then(response => {
dispatch(this.deleteStackSuccess(stack.stack_name));
})
.catch(error => {
dispatch(handleErrors(error, 'Stack could not be deleted'));
dispatch(this.deleteStackFailed());
});
};
}
};

View File

@ -14,20 +14,25 @@
* under the License.
*/
import { deploymentStates } from '../constants/DeploymentConstants';
import { get } from 'lodash';
import { normalize } from 'normalizr';
import { deploymentStates } from '../constants/DeploymentConstants';
import { getCurrentStack } from '../selectors/stacks';
import LoggerActions from './LoggerActions';
import NodesActions from './NodesActions';
import PlansActions from './PlansActions';
import RegisterNodesActions from './RegisterNodesActions';
import RolesActions from './RolesActions';
import StacksActions from './StacksActions';
import { stackSchema } from '../normalizrSchemas/stacks';
import MistralConstants from '../constants/MistralConstants';
import ZaqarWebSocketService from '../services/ZaqarWebSocketService';
import { handleWorkflowMessage } from './WorkflowActions';
import {
getDeploymentStatusSuccess,
deploymentFinished,
undeployFinished,
configDownloadMessage
} from './DeploymentActions';
import NetworksActions from './NetworksActions';
@ -142,12 +147,19 @@ export default {
break;
}
case MistralConstants.HEAT_STACKS_GET: {
const { stack, stack: { stack_name, id } } = payload;
dispatch(StacksActions.fetchStackSuccess(stack));
!getState().stacks.isFetchingResources &&
case MistralConstants.HEAT_STACKS_LIST: {
const stacks =
normalize(payload.stacks, [stackSchema]).entities.stacks || {};
dispatch(StacksActions.fetchStacksSuccess(stacks));
// TODO(jtomasek): It would be nicer if we could identify that
// stack has changed in the component and fetch resources there
const { isFetchingResources } = getState().stacks;
const currentStack = getCurrentStack(getState());
if (!isFetchingResources && currentStack) {
const { stack_name, id } = currentStack;
dispatch(StacksActions.fetchResources(stack_name, id));
break;
}
}
case MistralConstants.CONFIG_DOWNLOAD_DEPLOY: {
@ -170,6 +182,25 @@ export default {
break;
}
case MistralConstants.UNDEPLOY_PLAN: {
if (payload.deployment_status === deploymentStates.UNDEPLOYING) {
const { message, plan_name, deployment_status } = payload;
dispatch(
getDeploymentStatusSuccess(plan_name, {
status: deployment_status,
message
})
);
} else {
dispatch(
handleWorkflowMessage(payload.execution_id, execution =>
dispatch(undeployFinished(execution))
)
);
}
break;
}
case MistralConstants.PLAN_EXPORT: {
dispatch(
handleWorkflowMessage(payload.execution.id, execution =>

View File

@ -27,7 +27,6 @@ import {
CloseModalXButton,
RoutedModalPanel
} from '../ui/Modals';
import { deploymentStates } from '../../constants/DeploymentConstants';
import { getCurrentPlanName } from '../../selectors/plans';
import {
getCurrentPlanDeploymentStatus,
@ -91,14 +90,11 @@ class DeploymentConfirmation extends React.Component {
const {
allValidationsSuccessful,
currentPlanName,
deploymentStatus,
startDeployment,
environmentSummary
environmentSummary,
isPendingDeploymentRequest
} = this.props;
const buttonDisabled =
deploymentStatus.status === deploymentStates.STARTING_DEPLOYMENT;
return (
<RoutedModalPanel redirectPath={`/plans/${currentPlanName}`}>
<ModalHeader>
@ -132,7 +128,7 @@ class DeploymentConfirmation extends React.Component {
<FormattedMessage {...messages.deploymentConfirmation} />
</p>
<DeployButton
disabled={buttonDisabled}
disabled={isPendingDeploymentRequest}
deploy={startDeployment.bind(this, currentPlanName)}
/>
</BlankSlate>
@ -153,6 +149,7 @@ DeploymentConfirmation.propTypes = {
deploymentStatus: PropTypes.object.isRequired,
environmentSummary: PropTypes.string.isRequired,
intl: PropTypes.object,
isPendingDeploymentRequest: PropTypes.bool.isRequired,
runPreDeploymentValidations: PropTypes.func.isRequired,
startDeployment: PropTypes.func.isRequired
};
@ -163,7 +160,9 @@ const mapStateToProps = (state, props) => ({
deploymentStatusLoaded: getCurrentPlanDeploymentStatusUI(state).isLoaded,
deploymentStatus: getCurrentPlanDeploymentStatus(state),
deploymentStatusUIError: getCurrentPlanDeploymentStatusUI(state).error,
environmentSummary: getEnvironmentConfigurationSummary(state)
environmentSummary: getEnvironmentConfigurationSummary(state),
isPendingDeploymentRequest: getCurrentPlanDeploymentStatusUI(state)
.isPendingRequest
});
const mapDispatchToProps = dispatch => ({

View File

@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import DeploymentProgress from './DeploymentProgress';
import UndeployProgress from './UndeployProgress';
import DeploymentFailure from './DeploymentFailure';
import { deploymentStates } from '../../constants/DeploymentConstants';
import { getCurrentPlanName } from '../../selectors/plans';
@ -56,21 +57,12 @@ class DeploymentDetail extends React.Component {
switch (deploymentStatus.status) {
case deploymentStates.DEPLOYING:
case deploymentStates.UNDEPLOYING:
return <DeploymentProgress planName={currentPlanName} />;
case deploymentStates.DEPLOY_SUCCESS:
return (
<div>
{deploymentStatus.status}
{deploymentStatus.message}
</div>
);
case deploymentStates.UNDEPLOYING:
return <UndeployProgress planName={currentPlanName} />;
case deploymentStates.UNDEPLOY_FAILED:
case deploymentStates.DEPLOY_FAILED:
return <DeploymentFailure planName={currentPlanName} />;
case deploymentStates.UNDEPLOY_FAILED:
// TODO(jtomasek): handle undeploy failure
return 'undeploy failed';
case deploymentStates.UNKNOWN:
default:
return null;
}

View File

@ -24,10 +24,14 @@ import React from 'react';
import DeleteStackButton from '../deployment_plan/DeleteStackButton';
import { deploymentStatusMessages } from '../../constants/DeploymentConstants';
import { getCurrentStack } from '../../selectors/stacks';
import { getCurrentPlanDeploymentStatus } from '../../selectors/deployment';
import {
getCurrentPlanDeploymentStatus,
getCurrentPlanDeploymentStatusUI
} from '../../selectors/deployment';
import InlineNotification from '../ui/InlineNotification';
import { sanitizeMessage } from '../../utils';
import StacksActions from '../../actions/StacksActions';
import { startUndeploy } from '../../actions/DeploymentActions';
class DeploymentFailure extends React.Component {
componentDidMount() {
@ -38,9 +42,9 @@ class DeploymentFailure extends React.Component {
render() {
const {
deploymentStatus: { status, message },
deleteStack,
undeployPlan,
intl: { formatMessage },
isRequestingStackDelete,
isPendingRequest,
planName,
stack
} = this.props;
@ -53,36 +57,37 @@ class DeploymentFailure extends React.Component {
>
<p>{sanitizeMessage(message)}</p>
</InlineNotification>
<DeleteStackButton
deleteStack={deleteStack.bind(this, stack)}
disabled={isRequestingStackDelete || !stack}
loaded={!isRequestingStackDelete}
/>
{stack && (
<DeleteStackButton
deleteStack={undeployPlan.bind(this, planName)}
disabled={isPendingRequest}
/>
)}
</ModalBody>
);
}
}
DeploymentFailure.propTypes = {
deleteStack: PropTypes.func.isRequired,
deploymentStatus: PropTypes.object.isRequired,
fetchStacks: PropTypes.func.isRequired,
intl: PropTypes.object,
isFetchingStacks: PropTypes.bool.isRequired,
isRequestingStackDelete: PropTypes.bool.isRequired,
isPendingRequest: PropTypes.bool.isRequired,
planName: PropTypes.string.isRequired,
stack: ImmutablePropTypes.record
stack: ImmutablePropTypes.record,
undeployPlan: PropTypes.func.isRequired
};
const mapStateToProps = (state, props) => ({
deploymentStatus: getCurrentPlanDeploymentStatus(state),
isFetchingStacks: state.stacks.isFetching,
isRequestingStackDelete: state.stacks.isRequestingStackDelete,
isPendingRequest: getCurrentPlanDeploymentStatusUI(state).isPendingRequest,
stack: getCurrentStack(state)
});
const mapDispatchToProps = dispatch => ({
deleteStack: stack => dispatch(StacksActions.deleteStack(stack)),
undeployPlan: planName => dispatch(startUndeploy(planName)),
fetchStacks: () => dispatch(StacksActions.fetchStacks())
});

View File

@ -28,7 +28,7 @@ import {
getCreateCompleteResources
} from '../../selectors/stacks';
import { getCurrentPlanDeploymentStatus } from '../../selectors/deployment';
import { InlineLoader } from '../ui/Loader';
import { InlineLoader, Loader } from '../ui/Loader';
import StackResourcesTable from './StackResourcesTable';
import { stackStates } from '../../constants/StacksConstants';
@ -37,14 +37,6 @@ const messages = defineMessages({
id: 'DeploymentSuccess.resources',
defaultMessage: 'Resources'
},
cancelDeployment: {
id: 'DeploymentProgress.cancelDeployment',
defaultMessage: 'Cancel Deployment'
},
requestingDeletion: {
id: 'DeploymentProgress.requestingDeletion',
defaultMessage: 'Requesting Deletion of Deployment'
},
initializingDeployment: {
id: 'DeploymentProgress.initializingDeployment',
defaultMessage: 'Initializing {planName} plan deployment'
@ -70,77 +62,83 @@ class DeploymentProgress extends React.Component {
resources,
resourcesLoaded,
stackDeploymentProgress,
stack
stack,
stacksLoaded
} = this.props;
return (
<ModalBody className="flex-container">
{!stack && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.initializingDeployment}
values={{ planName: <strong>{planName}</strong> }}
/>
</div>
<ProgressBar
now={0}
label={<span>0%</span>}
className="progress-label-top-right"
/>
</Fragment>
)}
{stack &&
stack.stack_status !== stackStates.CREATE_COMPLETE && (
<Loader
loaded={stacksLoaded}
componentProps={{ className: 'flex-container' }}
>
{!stack && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.deployingPlan}
values={{
planName: <strong>{planName}</strong>,
resourcesCount,
completeResourcesCount
}}
/>
</div>
<ProgressBar
now={stackDeploymentProgress}
label={<span>{stackDeploymentProgress + '%'}</span>}
className="progress-label-top-right"
/>
{message && <pre>{message}</pre>}
<h2>
<FormattedMessage {...messages.resources} />
</h2>
<div className="flex-container">
<div className="flex-column">
<StackResourcesTable
isFetchingResources={!resourcesLoaded}
resources={resources.reverse()}
/>
</div>
</div>
</Fragment>
)}
{stack &&
stack.stack_status === stackStates.CREATE_COMPLETE && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.configuringPlan}
{...messages.initializingDeployment}
values={{ planName: <strong>{planName}</strong> }}
/>
</div>
<ProgressBar active striped now={100} />
{message && <pre>{message}</pre>}
<ConfigDownloadMessagesList
messages={configDownloadMessages.toJS()}
<ProgressBar
now={0}
label={<span>0%</span>}
className="progress-label-top-right"
/>
</Fragment>
)}
{stack &&
stack.stack_status !== stackStates.CREATE_COMPLETE && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.deployingPlan}
values={{
planName: <strong>{planName}</strong>,
resourcesCount,
completeResourcesCount
}}
/>
</div>
<ProgressBar
now={stackDeploymentProgress}
label={<span>{stackDeploymentProgress + '%'}</span>}
className="progress-label-top-right"
/>
{message && <pre>{message}</pre>}
<h2>
<FormattedMessage {...messages.resources} />
</h2>
<div className="flex-container">
<div className="flex-column">
<StackResourcesTable
isFetchingResources={!resourcesLoaded}
resources={resources.reverse()}
/>
</div>
</div>
</Fragment>
)}
{stack &&
stack.stack_status === stackStates.CREATE_COMPLETE && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.configuringPlan}
values={{ planName: <strong>{planName}</strong> }}
/>
</div>
<ProgressBar active striped now={100} />
{message && <pre>{message}</pre>}
<ConfigDownloadMessagesList
messages={configDownloadMessages.toJS()}
/>
</Fragment>
)}
</Loader>
</ModalBody>
);
}
@ -158,22 +156,23 @@ DeploymentProgress.propTypes = {
resourcesCount: PropTypes.number,
resourcesLoaded: PropTypes.bool.isRequired,
stack: ImmutablePropTypes.record,
stackDeploymentProgress: PropTypes.number.isRequired
stackDeploymentProgress: PropTypes.number.isRequired,
stacksLoaded: PropTypes.bool.isRequired
};
const mapStateToProps = (state, props) => ({
completeResourcesCount: getCreateCompleteResources(state).size,
stack: getCurrentStack(state),
deploymentStatus: getCurrentPlanDeploymentStatus(state),
isFetchingStacks: state.stacks.isFetching,
stackDeploymentProgress: getCurrentStackDeploymentProgress(state),
resourcesCount: state.stacks.resources.size,
resources: state.stacks.resources,
resourcesLoaded: state.stacks.resourcesLoaded
resourcesLoaded: state.stacks.resourcesLoaded,
stack: getCurrentStack(state),
stackDeploymentProgress: getCurrentStackDeploymentProgress(state),
stacksLoaded: state.stacks.isLoaded
});
const mapDispatchToProps = (dispatch, { planName }) => ({
deleteStack: () => dispatch(StacksActions.deleteStack(planName, '')),
fetchStacks: () => dispatch(StacksActions.fetchStacks()),
fetchResources: (stackName, stackId) =>
dispatch(StacksActions.fetchResources(stackName, stackId))

View File

@ -0,0 +1,167 @@
/**
* Copyright 2018 Red Hat Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import { connect } from 'react-redux';
import { defineMessages, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { ModalBody, ProgressBar } from 'react-bootstrap';
import PropTypes from 'prop-types';
import React, { Fragment } from 'react';
import {
getCurrentStack,
getCurrentStackDeletionProgress
} from '../../selectors/stacks';
import { getCurrentPlanDeploymentStatus } from '../../selectors/deployment';
import { InlineLoader, Loader } from '../ui/Loader';
import StackResourcesTable from './StackResourcesTable';
import { stackStates } from '../../constants/StacksConstants';
const messages = defineMessages({
resources: {
id: 'UndeployProgress.resources',
defaultMessage: 'Resources'
},
initializingUndeploy: {
id: 'UndeployProgress.initializingDeployment',
defaultMessage: 'Initializing {planName} plan deployment deletion'
},
undeployingPlan: {
id: 'UndeployProgress.undeployingPlan',
defaultMessage: 'Deleting {planName} plan deployment'
}
});
class UndeployProgress extends React.Component {
render() {
const {
deploymentStatus: { message },
planName,
resources,
resourcesLoaded,
stackDeletionProgress,
stack,
stacksLoaded
} = this.props;
return (
<ModalBody className="flex-container">
<Loader
loaded={stacksLoaded}
componentProps={{ className: 'flex-container' }}
>
{stack &&
stack.stack_status !== stackStates.DELETE_IN_PROGRESS && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.initializingUndeploy}
values={{ planName: <strong>{planName}</strong> }}
/>
</div>
<ProgressBar
now={0}
label={<span>0%</span>}
className="progress-label-top-right"
/>
</Fragment>
)}
{stack &&
stack.stack_status === stackStates.DELETE_IN_PROGRESS && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.undeployingPlan}
values={{ planName: <strong>{planName}</strong> }}
/>
</div>
<ProgressBar
bsStyle="danger"
now={stackDeletionProgress}
label={<span>{stackDeletionProgress + '%'}</span>}
className="progress-label-top-right"
/>
{message && <pre>{message}</pre>}
<h2>
<FormattedMessage {...messages.resources} />
</h2>
<div className="flex-container">
<div className="flex-column">
<StackResourcesTable
isFetchingResources={!resourcesLoaded}
resources={resources.reverse()}
/>
</div>
</div>
</Fragment>
)}
{!stack && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.undeployingPlan}
values={{ planName: <strong>{planName}</strong> }}
/>
</div>
<ProgressBar
bsStyle="danger"
now={100}
label={<span>{100 + '%'}</span>}
className="progress-label-top-right"
/>
{message && <pre>{message}</pre>}
</Fragment>
)}
</Loader>
</ModalBody>
);
}
}
UndeployProgress.propTypes = {
deploymentStatus: PropTypes.object.isRequired,
fetchResources: PropTypes.func.isRequired,
fetchStacks: PropTypes.func.isRequired,
intl: PropTypes.object,
isFetchingStacks: PropTypes.bool.isRequired,
planName: PropTypes.string.isRequired,
resources: ImmutablePropTypes.list,
resourcesLoaded: PropTypes.bool.isRequired,
stack: ImmutablePropTypes.record,
stackDeletionProgress: PropTypes.number.isRequired,
stacksLoaded: PropTypes.bool.isRequired
};
const mapStateToProps = (state, props) => ({
deploymentStatus: getCurrentPlanDeploymentStatus(state),
isFetchingStacks: state.stacks.isFetching,
resources: state.stacks.resources,
resourcesLoaded: state.stacks.resourcesLoaded,
stack: getCurrentStack(state),
stackDeletionProgress: getCurrentStackDeletionProgress(state),
stacksLoaded: state.stacks.isLoaded
});
const mapDispatchToProps = (dispatch, { planName }) => ({
fetchStacks: () => dispatch(StacksActions.fetchStacks()),
fetchResources: (stackName, stackId) =>
dispatch(StacksActions.fetchResources(stackName, stackId))
});
export default connect(mapStateToProps, mapDispatchToProps)(UndeployProgress);

View File

@ -62,7 +62,7 @@ class DeleteStackButton extends React.Component {
className="link btn btn-danger"
>
<InlineLoader
loaded={this.props.loaded}
loaded={!this.props.disabled}
content={formatMessage(messages.requestingDeletion)}
inverse
>
@ -85,8 +85,7 @@ class DeleteStackButton extends React.Component {
DeleteStackButton.propTypes = {
deleteStack: PropTypes.func.isRequired,
disabled: PropTypes.bool.isRequired,
intl: PropTypes.object,
loaded: PropTypes.bool.isRequired
intl: PropTypes.object
};
export default injectIntl(DeleteStackButton);

View File

@ -24,6 +24,7 @@ import React from 'react';
import DeploymentSuccess from './DeploymentSuccess';
import DeploymentFailure from './DeploymentFailure';
import DeploymentProgress from './DeploymentProgress';
import UndeployProgress from './UndeployProgress';
import {
deploymentStates,
deploymentStatusMessages
@ -49,20 +50,20 @@ const messages = defineMessages({
export const DeployStep = ({
currentPlan,
deploymentStatus,
deploymentStatusUIError,
intl: { formatMessage },
deploymentStatusUIError
isPendingDeploymentRequest
}) => {
switch (deploymentStatus.status) {
case deploymentStates.DEPLOYING:
case deploymentStates.UNDEPLOYING:
return <DeploymentProgress planName={currentPlan.name} />;
case deploymentStates.UNDEPLOYING:
return <UndeployProgress planName={currentPlan.name} />;
case deploymentStates.DEPLOY_SUCCESS:
return <DeploymentSuccess />;
case deploymentStates.DEPLOY_FAILED:
return <DeploymentFailure planName={currentPlan.name} />;
case deploymentStates.UNDEPLOY_FAILED:
// TODO(jtomasek): handle undeploy failure
return 'undeploy failed';
return <DeploymentFailure planName={currentPlan.name} />;
case deploymentStates.UNKNOWN:
return (
<InlineNotification
@ -74,16 +75,14 @@ export const DeployStep = ({
</InlineNotification>
);
default:
const disabled =
deploymentStatus.status === deploymentStates.STARTING_DEPLOYMENT;
return (
<Link
to={`/plans/${currentPlan.name}/deployment-confirmation`}
className="btn btn-primary btn-lg link"
disabled={disabled}
disabled={isPendingDeploymentRequest}
>
<InlineLoader
loaded={!disabled}
loaded={!isPendingDeploymentRequest}
content={formatMessage(messages.requestingDeploy)}
>
<FormattedMessage {...messages.validateAndDeploy} />
@ -97,12 +96,15 @@ DeployStep.propTypes = {
currentPlan: ImmutablePropTypes.record.isRequired,
deploymentStatus: PropTypes.object.isRequired,
deploymentStatusUIError: PropTypes.string,
intl: PropTypes.object
intl: PropTypes.object,
isPendingDeploymentRequest: PropTypes.bool.isRequired
};
const mapStateToProps = (state, props) => ({
deploymentStatus: getCurrentPlanDeploymentStatus(state),
deploymentStatusUIError: getCurrentPlanDeploymentStatusUI(state).error
deploymentStatusUIError: getCurrentPlanDeploymentStatusUI(state).error,
isPendingDeploymentRequest: getCurrentPlanDeploymentStatusUI(state)
.isPendingRequest
});
export default injectIntl(connect(mapStateToProps)(DeployStep));

View File

@ -22,9 +22,7 @@ import DeploymentConfirmation from '../deployment/DeploymentConfirmation';
import { deploymentStates as ds } from '../../constants/DeploymentConstants';
const DeploymentConfirmationRoute = ({ currentPlanName, deploymentStatus }) =>
[ds.UNDEPLOYED, ds.UNKNOWN, ds.STARTING_DEPLOYMENT].includes(
deploymentStatus
) ? (
[ds.UNDEPLOYED, ds.UNKNOWN].includes(deploymentStatus) ? (
<DeploymentConfirmation />
) : (
<Redirect to={`/plans/${currentPlanName}`} />

View File

@ -22,7 +22,7 @@ import DeploymentDetail from '../deployment/DeploymentDetail';
import { deploymentStates as ds } from '../../constants/DeploymentConstants';
const DeploymentDetailRoute = ({ currentPlanName, deploymentStatus }) =>
[ds.DEPLOYING, ds.UNDEPLOYING, ds.DEPLOY_FAILED].includes(
[ds.DEPLOYING, ds.UNDEPLOYING, ds.DEPLOY_FAILED, ds.UNDEPLOY_FAILED].includes(
deploymentStatus
) ? (
<DeploymentDetail />

View File

@ -30,18 +30,10 @@ import {
getCurrentStackDeploymentProgress,
getCreateCompleteResources
} from '../../selectors/stacks';
import { InlineLoader } from '../ui/Loader';
import { InlineLoader, Loader } from '../ui/Loader';
import StacksActions from '../../actions/StacksActions';
const messages = defineMessages({
cancelDeployment: {
id: 'DeploymentProgress.cancelDeployment',
defaultMessage: 'Cancel Deployment'
},
requestingDeletion: {
id: 'DeploymentProgress.requestingDeletion',
defaultMessage: 'Requesting Deletion of Deployment'
},
initializingDeployment: {
id: 'DeploymentProgress.initializingDeployment',
defaultMessage: 'Initializing {planName} plan deployment'
@ -66,10 +58,10 @@ class DeploymentProgress extends React.Component {
this.fetchStacks();
}
componentWillReceiveProps(newProps) {
if (!this.props.stack && newProps.stack) {
const { stack: { stack_name, id } } = newProps;
this.props.fetchResources(stack_name, id);
componentDidUpdate(prevProps) {
if (!prevProps.stack && this.props.stack) {
const { stack: { stack_name, id }, fetchResources } = this.props;
fetchResources(stack_name, id);
}
}
@ -85,7 +77,8 @@ class DeploymentProgress extends React.Component {
resourcesCount,
completeResourcesCount,
stackDeploymentProgress,
stack
stack,
stacksLoaded
} = this.props;
return (
@ -101,58 +94,60 @@ class DeploymentProgress extends React.Component {
<FormattedMessage {...messages.viewInformation} />
</Link>
</p>
{!stack && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.initializingDeployment}
values={{ planName: <strong>{planName}</strong> }}
/>
</div>
<ProgressBar
now={0}
label={<span>0%</span>}
className="progress-label-top-right"
/>
</Fragment>
)}
{stack &&
stack.stack_status !== stackStates.CREATE_COMPLETE && (
<Loader loaded={stacksLoaded}>
{!stack && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.deployingPlan}
values={{
planName: <strong>{planName}</strong>,
resourcesCount,
completeResourcesCount
}}
/>
</div>
<ProgressBar
now={stackDeploymentProgress}
label={<span>{stackDeploymentProgress + '%'}</span>}
className="progress-label-top-right"
/>
{message && <pre>{message}</pre>}
</Fragment>
)}
{stack &&
stack.stack_status === stackStates.CREATE_COMPLETE && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.configuringPlan}
{...messages.initializingDeployment}
values={{ planName: <strong>{planName}</strong> }}
/>
</div>
<ProgressBar active striped now={100} />
{message && <pre>{message}</pre>}
<ProgressBar
now={0}
label={<span>0%</span>}
className="progress-label-top-right"
/>
</Fragment>
)}
{stack &&
stack.stack_status !== stackStates.CREATE_COMPLETE && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.deployingPlan}
values={{
planName: <strong>{planName}</strong>,
resourcesCount,
completeResourcesCount
}}
/>
</div>
<ProgressBar
now={stackDeploymentProgress}
label={<span>{stackDeploymentProgress + '%'}</span>}
className="progress-label-top-right"
/>
{message && <pre>{message}</pre>}
</Fragment>
)}
{stack &&
stack.stack_status === stackStates.CREATE_COMPLETE && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.configuringPlan}
values={{ planName: <strong>{planName}</strong> }}
/>
</div>
<ProgressBar active striped now={100} />
{message && <pre>{message}</pre>}
</Fragment>
)}
</Loader>
</div>
);
}
@ -168,7 +163,8 @@ DeploymentProgress.propTypes = {
planName: PropTypes.string.isRequired,
resourcesCount: PropTypes.number,
stack: ImmutablePropTypes.record,
stackDeploymentProgress: PropTypes.number.isRequired
stackDeploymentProgress: PropTypes.number.isRequired,
stacksLoaded: PropTypes.bool.isRequired
};
const mapStateToProps = (state, props) => ({
@ -176,12 +172,12 @@ const mapStateToProps = (state, props) => ({
stack: getCurrentStack(state),
deploymentStatus: getCurrentPlanDeploymentStatus(state),
isFetchingStacks: state.stacks.isFetching,
stacksLoaded: state.stacks.isLoaded,
stackDeploymentProgress: getCurrentStackDeploymentProgress(state),
resourcesCount: state.stacks.resources.size
});
const mapDispatchToProps = (dispatch, { planName }) => ({
deleteStack: () => dispatch(StacksActions.deleteStack(planName, '')),
fetchStacks: () => dispatch(StacksActions.fetchStacks()),
fetchResources: (stackName, stackId) =>
dispatch(StacksActions.fetchResources(stackName, stackId))

View File

@ -22,11 +22,16 @@ import React, { Fragment } from 'react';
import DeleteStackButton from './DeleteStackButton';
import { deploymentStatusMessages } from '../../constants/DeploymentConstants';
import { getCurrentPlanDeploymentStatus } from '../../selectors/deployment';
import {
getCurrentPlanDeploymentStatus,
getCurrentPlanDeploymentStatusUI
} from '../../selectors/deployment';
import { getCurrentStack, getOvercloudInfo } from '../../selectors/stacks';
import { getCurrentPlanName } from '../../selectors/plans';
import InlineNotification from '../ui/InlineNotification';
import OvercloudInfo from '../deployment/OvercloudInfo';
import { Loader } from '../ui/Loader';
import { startUndeploy } from '../../actions/DeploymentActions';
import StacksActions from '../../actions/StacksActions';
class DeploymentSuccess extends React.Component {
@ -43,12 +48,13 @@ class DeploymentSuccess extends React.Component {
render() {
const {
intl: { formatMessage },
isPendingRequest,
stack,
stacksLoaded,
overcloudInfo,
deleteStack,
deploymentStatus: { status, message },
isRequestingStackDelete
planName,
undeployPlan,
deploymentStatus: { status, message }
} = this.props;
return (
@ -67,9 +73,8 @@ class DeploymentSuccess extends React.Component {
fetchOvercloudInfo={this.fetchOvercloudInfo.bind(this)}
/>
<DeleteStackButton
deleteStack={deleteStack.bind(this, stack)}
disabled={isRequestingStackDelete}
loaded={!isRequestingStackDelete}
deleteStack={undeployPlan.bind(this, planName)}
disabled={isPendingRequest}
/>
</Fragment>
)}
@ -79,28 +84,30 @@ class DeploymentSuccess extends React.Component {
}
DeploymentSuccess.propTypes = {
deleteStack: PropTypes.func.isRequired,
deploymentStatus: ImmutablePropTypes.record.isRequired,
fetchStackEnvironment: PropTypes.func.isRequired,
fetchStackResource: PropTypes.func.isRequired,
fetchStacks: PropTypes.func.isRequired,
intl: PropTypes.object,
isRequestingStackDelete: PropTypes.bool,
isPendingRequest: PropTypes.bool.isRequired,
overcloudInfo: ImmutablePropTypes.map.isRequired,
planName: PropTypes.string.isRequired,
stack: ImmutablePropTypes.record,
stacksLoaded: PropTypes.bool.isRequired
stacksLoaded: PropTypes.bool.isRequired,
undeployPlan: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
deploymentStatus: getCurrentPlanDeploymentStatus(state),
isRequestingStackDelete: state.stacks.isRequestingStackDelete,
planName: getCurrentPlanName(state),
overcloudInfo: getOvercloudInfo(state),
stack: getCurrentStack(state),
stacksLoaded: state.stacks.isLoaded
stacksLoaded: state.stacks.isLoaded,
isPendingRequest: getCurrentPlanDeploymentStatusUI(state).isPendingRequest
});
const mapDispatchToProps = dispatch => ({
deleteStack: planName => dispatch(StacksActions.deleteStack(planName, '')),
undeployPlan: planName => dispatch(startUndeploy(planName)),
fetchStacks: () => dispatch(StacksActions.fetchStacks()),
fetchStackEnvironment: stack =>
dispatch(StacksActions.fetchEnvironment(stack)),

View File

@ -0,0 +1,183 @@
/**
* Copyright 2018 Red Hat Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import { connect } from 'react-redux';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router-dom';
import { ProgressBar } from 'react-bootstrap';
import PropTypes from 'prop-types';
import React, { Fragment } from 'react';
import { deploymentStatusMessages } from '../../constants/DeploymentConstants';
import { stackStates } from '../../constants/StacksConstants';
import { getCurrentPlanDeploymentStatus } from '../../selectors/deployment';
import {
getCurrentStack,
getCurrentStackDeletionProgress,
getDeleteCompleteResources
} from '../../selectors/stacks';
import { InlineLoader, Loader } from '../ui/Loader';
import StacksActions from '../../actions/StacksActions';
const messages = defineMessages({
initializingUndeploy: {
id: 'UndeployProgress.initializingUndeploy',
defaultMessage: 'Initializing {planName} plan deployment deletion'
},
viewInformation: {
id: 'UndeployProgress.viewInformation',
defaultMessage: 'View detailed information'
},
undeployingPlan: {
id: 'UndeployProgress.undeployingPlan',
defaultMessage: 'Deleting {planName} plan deployment'
}
});
class UndeployProgress extends React.Component {
componentDidMount() {
this.fetchStacks();
}
componentDidUpdate(prevProps) {
if (!prevProps.stack && this.props.stack) {
const { stack: { stack_name, id }, fetchResources } = this.props;
fetchResources(stack_name, id);
}
}
fetchStacks() {
const { fetchStacks, isFetchingStacks } = this.props;
!isFetchingStacks && fetchStacks();
}
render() {
const {
deploymentStatus: { status, message },
planName,
stackDeletionProgress,
stack,
stacksLoaded
} = this.props;
return (
<div>
<p>
<span>
<FormattedMessage
{...deploymentStatusMessages[status]}
values={{ planName: <strong>{planName}</strong> }}
/>
</span>{' '}
<Link to={`/plans/${planName}/deployment-detail`}>
<FormattedMessage {...messages.viewInformation} />
</Link>
</p>
<Loader loaded={stacksLoaded}>
{stack &&
stack.stack_status !== stackStates.DELETE_IN_PROGRESS && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.initializingUndeploy}
values={{ planName: <strong>{planName}</strong> }}
/>
</div>
<ProgressBar
now={0}
label={<span>0%</span>}
className="progress-label-top-right"
/>
</Fragment>
)}
{stack &&
stack.stack_status === stackStates.DELETE_IN_PROGRESS && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.undeployingPlan}
values={{ planName: <strong>{planName}</strong> }}
/>
</div>
<ProgressBar
bsStyle="danger"
now={stackDeletionProgress}
label={<span>{stackDeletionProgress + '%'}</span>}
className="progress-label-top-right"
/>
{message && <pre>{message}</pre>}
</Fragment>
)}
{!stack && (
<Fragment>
<div className="progress-description">
<InlineLoader />
<FormattedMessage
{...messages.undeployingPlan}
values={{ planName: <strong>{planName}</strong> }}
/>
</div>
<ProgressBar
bsStyle="danger"
now={100}
label={<span>{100 + '%'}</span>}
className="progress-label-top-right"
/>
{message && <pre>{message}</pre>}
</Fragment>
)}
</Loader>
</div>
);
}
}
UndeployProgress.propTypes = {
deletedResourcesCount: PropTypes.number,
deploymentStatus: PropTypes.object.isRequired,
fetchResources: PropTypes.func.isRequired,
fetchStacks: PropTypes.func.isRequired,
intl: PropTypes.object,
isFetchingStacks: PropTypes.bool.isRequired,
planName: PropTypes.string.isRequired,
resourcesCount: PropTypes.number,
stack: ImmutablePropTypes.record,
stackDeletionProgress: PropTypes.number.isRequired,
stacksLoaded: PropTypes.bool.isRequired
};
const mapStateToProps = (state, props) => ({
deletedResourcesCount: getDeleteCompleteResources(state).size,
deploymentStatus: getCurrentPlanDeploymentStatus(state),
isFetchingStacks: state.stacks.isFetching,
stack: getCurrentStack(state),
stacksLoaded: state.stacks.isLoaded,
stackDeletionProgress: getCurrentStackDeletionProgress(state),
resourcesCount: state.stacks.resources.size
});
const mapDispatchToProps = (dispatch, { planName }) => ({
fetchStacks: () => dispatch(StacksActions.fetchStacks()),
fetchResources: (stackName, stackId) =>
dispatch(StacksActions.fetchResources(stackName, stackId))
});
export default injectIntl(
connect(mapStateToProps, mapDispatchToProps)(UndeployProgress)
);

View File

@ -26,11 +26,15 @@ export const START_DEPLOYMENT_SUCCESS = 'START_DEPLOYMENT_SUCCESS';
export const START_DEPLOYMENT_PENDING = 'START_DEPLOYMENT_PENDING';
export const DEPLOYMENT_FAILED = 'DEPLOYMENT_FAILED';
export const DEPLOYMENT_SUCCESS = 'DEPLOYMENT_SUCCESS';
export const START_UNDEPLOY_FAILED = 'START_UNDEPLOY_FAILED';
export const START_UNDEPLOY_SUCCESS = 'START_UNDEPLOY_SUCCESS';
export const START_UNDEPLOY_PENDING = 'START_UNDEPLOY_PENDING';
export const UNDEPLOY_FAILED = 'UNDEPLOY_FAILED';
export const UNDEPLOY_SUCCESS = 'UNDEPLOY_SUCCESS';
export const deploymentStates = keyMirror({
UNDEPLOYED: null,
DEPLOY_SUCCESS: null,
STARTING_DEPLOYMENT: null,
DEPLOYING: null,
UNDEPLOYING: null,
DEPLOY_FAILED: null,
@ -53,7 +57,8 @@ export const deploymentStatusMessages = defineMessages({
},
UNDEPLOYING: {
id: 'DeploymentStatus.undeploying',
defaultMessage: 'Undeploy in progress'
defaultMessage:
'Deletion of {planName} plan deployment is currently in progress'
},
DEPLOY_FAILED: {
id: 'DeploymentStatus.deploymentFailed',

View File

@ -30,7 +30,7 @@ export default {
DEPLOYMENT_DEPLOY_PLAN: 'tripleo.deployment.v1.deploy_plan',
UNDEPLOY_PLAN: 'tripleo.deployment.v1.undeploy_plan',
DOWNLOAD_LOGS: 'tripleo.plan_management.v1.download_logs',
HEAT_STACKS_GET: 'tripleo.stack.v1._heat_stacks_get',
HEAT_STACKS_LIST: 'tripleo.stack.v1._heat_stacks_list',
NETWORK_LIST: 'tripleo.plan_management.v1.list_networks',
PARAMETERS_GET: 'tripleo.parameters.get_flatten',
PARAMETERS_UPDATE: 'tripleo.parameters.update',

View File

@ -18,9 +18,6 @@ import keyMirror from 'keymirror';
import { defineMessages } from 'react-intl';
export default keyMirror({
DELETE_STACK_PENDING: null,
DELETE_STACK_FAILED: null,
DELETE_STACK_SUCCESS: null,
FETCH_STACK_ENVIRONMENT_SUCCESS: null,
FETCH_STACK_ENVIRONMENT_PENDING: null,
FETCH_STACK_ENVIRONMENT_FAILED: null,

View File

@ -28,5 +28,6 @@ export const DeploymentStatus = Record({
export const DeploymentStatusUI = Record({
error: undefined,
isLoaded: false,
isFetching: false
isFetching: false,
isPendingRequest: false
});

View File

@ -18,7 +18,6 @@ import { List, Map, Record } from 'immutable';
export const StacksState = Record({
currentStackEnvironment: Map(),
isRequestingStackDelete: false,
isLoaded: false,
isFetching: false,
isFetchingResources: false,

View File

@ -27,6 +27,11 @@ import {
START_DEPLOYMENT_FAILED,
START_DEPLOYMENT_PENDING,
START_DEPLOYMENT_SUCCESS,
START_UNDEPLOY_FAILED,
START_UNDEPLOY_PENDING,
START_UNDEPLOY_SUCCESS,
UNDEPLOY_FAILED,
UNDEPLOY_SUCCESS,
deploymentStates
} from '../constants/DeploymentConstants';
import {
@ -51,24 +56,11 @@ export const deploymentStatusByPlan = (state = Map(), { type, payload }) => {
messages.push(payload.message)
)
);
case START_DEPLOYMENT_PENDING:
return state.set(
payload,
new DeploymentStatus({ status: deploymentStates.STARTING_DEPLOYMENT })
);
case START_DEPLOYMENT_SUCCESS:
return state.set(
payload,
new DeploymentStatus({ status: deploymentStates.DEPLOYING })
);
case START_DEPLOYMENT_FAILED:
return state.set(
payload.planName,
new DeploymentStatus({
status: deploymentStates.UNDEPLOYED,
message: payload.message
})
);
case DEPLOYMENT_SUCCESS:
return state.set(
payload.planName,
@ -85,6 +77,27 @@ export const deploymentStatusByPlan = (state = Map(), { type, payload }) => {
message: payload.message
})
);
case START_UNDEPLOY_SUCCESS:
return state.set(
payload,
new DeploymentStatus({ status: deploymentStates.UNDEPLOYING })
);
case UNDEPLOY_SUCCESS:
return state.set(
payload.planName,
new DeploymentStatus({
status: deploymentStates.UNDEPLOYED,
message: payload.message
})
);
case UNDEPLOY_FAILED:
return state.set(
payload.planName,
new DeploymentStatus({
status: deploymentStates.UNDEPLOY_FAILED,
message: payload.message
})
);
default:
return state;
}
@ -112,6 +125,14 @@ export const deploymentStatusUI = (state = Map(), { type, payload }) => {
error: undefined
})
);
case START_DEPLOYMENT_PENDING:
case START_UNDEPLOY_PENDING:
return state.setIn([payload, 'isPendingRequest'], true);
case START_DEPLOYMENT_SUCCESS:
case START_DEPLOYMENT_FAILED:
case START_UNDEPLOY_SUCCESS:
case START_UNDEPLOY_FAILED:
return state.setIn([payload, 'isPendingRequest'], false);
default:
return state;
}

View File

@ -17,7 +17,7 @@
import { fromJS, Map } from 'immutable';
import { Stack, StackResource, StacksState } from '../immutableRecords/stacks';
import StacksConstants, { stackStates } from '../constants/StacksConstants';
import StacksConstants from '../constants/StacksConstants';
import PlansConstants from '../constants/PlansConstants';
const initialState = new StacksState();
@ -90,20 +90,6 @@ export default function stacksReducer(state = initialState, action) {
case PlansConstants.PLAN_CHOSEN:
return initialState;
case StacksConstants.DELETE_STACK_SUCCESS:
return state
.set('isRequestingStackDelete', false)
.setIn(
['stacks', action.payload, 'stack_status'],
stackStates.DELETE_IN_PROGRESS
);
case StacksConstants.DELETE_STACK_FAILED:
return state.set('isRequestingStackDelete', false);
case StacksConstants.DELETE_STACK_PENDING:
return state.set('isRequestingStackDelete', true);
default:
return state;
}

View File

@ -57,6 +57,11 @@ export const getCreateCompleteResources = createSelector(
resources => resources.filter(r => r.resource_status === 'CREATE_COMPLETE')
);
export const getDeleteCompleteResources = createSelector(
[stackResourcesSelector],
resources => resources.filter(r => r.resource_status === 'DELETE_COMPLETE')
);
/**
* Returns calculated percentage of deployment progress
*/
@ -71,6 +76,20 @@ export const getCurrentStackDeploymentProgress = createSelector(
}
);
/**
* Returns calculated percentage of deletion progress
*/
export const getCurrentStackDeletionProgress = createSelector(
[stackResourcesSelector, getDeleteCompleteResources],
(resources, completeResources) => {
let allResources = resources.size;
if (allResources > 0) {
return Math.ceil(completeResources.size / allResources * 100);
}
return 0;
}
);
/**
* Returns a Map containing the overcloud information.
*/