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_PENDING,
START_DEPLOYMENT_SUCCESS, START_DEPLOYMENT_SUCCESS,
DEPLOYMENT_FAILED, DEPLOYMENT_FAILED,
DEPLOYMENT_SUCCESS DEPLOYMENT_SUCCESS,
START_UNDEPLOY_FAILED,
START_UNDEPLOY_PENDING,
START_UNDEPLOY_SUCCESS,
UNDEPLOY_FAILED,
UNDEPLOY_SUCCESS
} from '../constants/DeploymentConstants'; } from '../constants/DeploymentConstants';
import { handleErrors } from './ErrorActions'; import { handleErrors } from './ErrorActions';
import MistralConstants from '../constants/MistralConstants'; import MistralConstants from '../constants/MistralConstants';
@ -91,9 +96,9 @@ export const startDeploymentSuccess = planName => ({
payload: planName payload: planName
}); });
export const startDeploymentFailed = (planName, message) => ({ export const startDeploymentFailed = planName => ({
type: START_DEPLOYMENT_FAILED, type: START_DEPLOYMENT_FAILED,
payload: { planName, message } payload: planName
}); });
export const startDeployment = planName => dispatch => { export const startDeployment = planName => dispatch => {
@ -143,3 +148,67 @@ export const deploymentFinished = execution => (
dispatch(deploymentSuccess(planName, message)); 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)); 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. * under the License.
*/ */
import { deploymentStates } from '../constants/DeploymentConstants';
import { get } from 'lodash'; import { get } from 'lodash';
import { normalize } from 'normalizr';
import { deploymentStates } from '../constants/DeploymentConstants';
import { getCurrentStack } from '../selectors/stacks';
import LoggerActions from './LoggerActions'; import LoggerActions from './LoggerActions';
import NodesActions from './NodesActions'; import NodesActions from './NodesActions';
import PlansActions from './PlansActions'; import PlansActions from './PlansActions';
import RegisterNodesActions from './RegisterNodesActions'; import RegisterNodesActions from './RegisterNodesActions';
import RolesActions from './RolesActions'; import RolesActions from './RolesActions';
import StacksActions from './StacksActions'; import StacksActions from './StacksActions';
import { stackSchema } from '../normalizrSchemas/stacks';
import MistralConstants from '../constants/MistralConstants'; import MistralConstants from '../constants/MistralConstants';
import ZaqarWebSocketService from '../services/ZaqarWebSocketService'; import ZaqarWebSocketService from '../services/ZaqarWebSocketService';
import { handleWorkflowMessage } from './WorkflowActions'; import { handleWorkflowMessage } from './WorkflowActions';
import { import {
getDeploymentStatusSuccess, getDeploymentStatusSuccess,
deploymentFinished, deploymentFinished,
undeployFinished,
configDownloadMessage configDownloadMessage
} from './DeploymentActions'; } from './DeploymentActions';
import NetworksActions from './NetworksActions'; import NetworksActions from './NetworksActions';
@ -142,12 +147,19 @@ export default {
break; break;
} }
case MistralConstants.HEAT_STACKS_GET: { case MistralConstants.HEAT_STACKS_LIST: {
const { stack, stack: { stack_name, id } } = payload; const stacks =
dispatch(StacksActions.fetchStackSuccess(stack)); normalize(payload.stacks, [stackSchema]).entities.stacks || {};
!getState().stacks.isFetchingResources && 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)); dispatch(StacksActions.fetchResources(stack_name, id));
break; }
} }
case MistralConstants.CONFIG_DOWNLOAD_DEPLOY: { case MistralConstants.CONFIG_DOWNLOAD_DEPLOY: {
@ -170,6 +182,25 @@ export default {
break; 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: { case MistralConstants.PLAN_EXPORT: {
dispatch( dispatch(
handleWorkflowMessage(payload.execution.id, execution => handleWorkflowMessage(payload.execution.id, execution =>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,11 +22,16 @@ import React, { Fragment } from 'react';
import DeleteStackButton from './DeleteStackButton'; import DeleteStackButton from './DeleteStackButton';
import { deploymentStatusMessages } from '../../constants/DeploymentConstants'; import { deploymentStatusMessages } from '../../constants/DeploymentConstants';
import { getCurrentPlanDeploymentStatus } from '../../selectors/deployment'; import {
getCurrentPlanDeploymentStatus,
getCurrentPlanDeploymentStatusUI
} from '../../selectors/deployment';
import { getCurrentStack, getOvercloudInfo } from '../../selectors/stacks'; import { getCurrentStack, getOvercloudInfo } from '../../selectors/stacks';
import { getCurrentPlanName } from '../../selectors/plans';
import InlineNotification from '../ui/InlineNotification'; import InlineNotification from '../ui/InlineNotification';
import OvercloudInfo from '../deployment/OvercloudInfo'; import OvercloudInfo from '../deployment/OvercloudInfo';
import { Loader } from '../ui/Loader'; import { Loader } from '../ui/Loader';
import { startUndeploy } from '../../actions/DeploymentActions';
import StacksActions from '../../actions/StacksActions'; import StacksActions from '../../actions/StacksActions';
class DeploymentSuccess extends React.Component { class DeploymentSuccess extends React.Component {
@ -43,12 +48,13 @@ class DeploymentSuccess extends React.Component {
render() { render() {
const { const {
intl: { formatMessage }, intl: { formatMessage },
isPendingRequest,
stack, stack,
stacksLoaded, stacksLoaded,
overcloudInfo, overcloudInfo,
deleteStack, planName,
deploymentStatus: { status, message }, undeployPlan,
isRequestingStackDelete deploymentStatus: { status, message }
} = this.props; } = this.props;
return ( return (
@ -67,9 +73,8 @@ class DeploymentSuccess extends React.Component {
fetchOvercloudInfo={this.fetchOvercloudInfo.bind(this)} fetchOvercloudInfo={this.fetchOvercloudInfo.bind(this)}
/> />
<DeleteStackButton <DeleteStackButton
deleteStack={deleteStack.bind(this, stack)} deleteStack={undeployPlan.bind(this, planName)}
disabled={isRequestingStackDelete} disabled={isPendingRequest}
loaded={!isRequestingStackDelete}
/> />
</Fragment> </Fragment>
)} )}
@ -79,28 +84,30 @@ class DeploymentSuccess extends React.Component {
} }
DeploymentSuccess.propTypes = { DeploymentSuccess.propTypes = {
deleteStack: PropTypes.func.isRequired,
deploymentStatus: ImmutablePropTypes.record.isRequired, deploymentStatus: ImmutablePropTypes.record.isRequired,
fetchStackEnvironment: PropTypes.func.isRequired, fetchStackEnvironment: PropTypes.func.isRequired,
fetchStackResource: PropTypes.func.isRequired, fetchStackResource: PropTypes.func.isRequired,
fetchStacks: PropTypes.func.isRequired, fetchStacks: PropTypes.func.isRequired,
intl: PropTypes.object, intl: PropTypes.object,
isRequestingStackDelete: PropTypes.bool, isPendingRequest: PropTypes.bool.isRequired,
overcloudInfo: ImmutablePropTypes.map.isRequired, overcloudInfo: ImmutablePropTypes.map.isRequired,
planName: PropTypes.string.isRequired,
stack: ImmutablePropTypes.record, stack: ImmutablePropTypes.record,
stacksLoaded: PropTypes.bool.isRequired stacksLoaded: PropTypes.bool.isRequired,
undeployPlan: PropTypes.func.isRequired
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
deploymentStatus: getCurrentPlanDeploymentStatus(state), deploymentStatus: getCurrentPlanDeploymentStatus(state),
isRequestingStackDelete: state.stacks.isRequestingStackDelete, planName: getCurrentPlanName(state),
overcloudInfo: getOvercloudInfo(state), overcloudInfo: getOvercloudInfo(state),
stack: getCurrentStack(state), stack: getCurrentStack(state),
stacksLoaded: state.stacks.isLoaded stacksLoaded: state.stacks.isLoaded,
isPendingRequest: getCurrentPlanDeploymentStatusUI(state).isPendingRequest
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
deleteStack: planName => dispatch(StacksActions.deleteStack(planName, '')), undeployPlan: planName => dispatch(startUndeploy(planName)),
fetchStacks: () => dispatch(StacksActions.fetchStacks()), fetchStacks: () => dispatch(StacksActions.fetchStacks()),
fetchStackEnvironment: stack => fetchStackEnvironment: stack =>
dispatch(StacksActions.fetchEnvironment(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 START_DEPLOYMENT_PENDING = 'START_DEPLOYMENT_PENDING';
export const DEPLOYMENT_FAILED = 'DEPLOYMENT_FAILED'; export const DEPLOYMENT_FAILED = 'DEPLOYMENT_FAILED';
export const DEPLOYMENT_SUCCESS = 'DEPLOYMENT_SUCCESS'; 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({ export const deploymentStates = keyMirror({
UNDEPLOYED: null, UNDEPLOYED: null,
DEPLOY_SUCCESS: null, DEPLOY_SUCCESS: null,
STARTING_DEPLOYMENT: null,
DEPLOYING: null, DEPLOYING: null,
UNDEPLOYING: null, UNDEPLOYING: null,
DEPLOY_FAILED: null, DEPLOY_FAILED: null,
@ -53,7 +57,8 @@ export const deploymentStatusMessages = defineMessages({
}, },
UNDEPLOYING: { UNDEPLOYING: {
id: 'DeploymentStatus.undeploying', id: 'DeploymentStatus.undeploying',
defaultMessage: 'Undeploy in progress' defaultMessage:
'Deletion of {planName} plan deployment is currently in progress'
}, },
DEPLOY_FAILED: { DEPLOY_FAILED: {
id: 'DeploymentStatus.deploymentFailed', id: 'DeploymentStatus.deploymentFailed',

View File

@ -30,7 +30,7 @@ export default {
DEPLOYMENT_DEPLOY_PLAN: 'tripleo.deployment.v1.deploy_plan', DEPLOYMENT_DEPLOY_PLAN: 'tripleo.deployment.v1.deploy_plan',
UNDEPLOY_PLAN: 'tripleo.deployment.v1.undeploy_plan', UNDEPLOY_PLAN: 'tripleo.deployment.v1.undeploy_plan',
DOWNLOAD_LOGS: 'tripleo.plan_management.v1.download_logs', 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', NETWORK_LIST: 'tripleo.plan_management.v1.list_networks',
PARAMETERS_GET: 'tripleo.parameters.get_flatten', PARAMETERS_GET: 'tripleo.parameters.get_flatten',
PARAMETERS_UPDATE: 'tripleo.parameters.update', PARAMETERS_UPDATE: 'tripleo.parameters.update',

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@
import { fromJS, Map } from 'immutable'; import { fromJS, Map } from 'immutable';
import { Stack, StackResource, StacksState } from '../immutableRecords/stacks'; import { Stack, StackResource, StacksState } from '../immutableRecords/stacks';
import StacksConstants, { stackStates } from '../constants/StacksConstants'; import StacksConstants from '../constants/StacksConstants';
import PlansConstants from '../constants/PlansConstants'; import PlansConstants from '../constants/PlansConstants';
const initialState = new StacksState(); const initialState = new StacksState();
@ -90,20 +90,6 @@ export default function stacksReducer(state = initialState, action) {
case PlansConstants.PLAN_CHOSEN: case PlansConstants.PLAN_CHOSEN:
return initialState; 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: default:
return state; return state;
} }

View File

@ -57,6 +57,11 @@ export const getCreateCompleteResources = createSelector(
resources => resources.filter(r => r.resource_status === 'CREATE_COMPLETE') 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 * 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. * Returns a Map containing the overcloud information.
*/ */