Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

Commit

Permalink
Merge pull request #798 from mturley/786-plan-denied-state
Browse files Browse the repository at this point in the history
[#786] Handle denied state of a plan request (no conversion hosts configured)

(cherry picked from commit e8e89d1)

https://bugzilla.redhat.com/show_bug.cgi?id=1640816
  • Loading branch information
michaelkro authored and simaishi committed Nov 26, 2018
1 parent e3d8fca commit 9b78d09
Show file tree
Hide file tree
Showing 16 changed files with 218 additions and 36 deletions.
10 changes: 8 additions & 2 deletions app/javascript/react/screens/App/Overview/Overview.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,9 @@ class Overview extends React.Component {
fetchTransformationMappingsAction,
openMappingWizardOnTransitionAction,
setMigrationsFilterAction,
initialMigrationsFilterSet
initialMigrationsFilterSet,
acknowledgeDeniedPlanRequestAction,
isEditingPlanRequest
} = this.props;

const mainContent = (
Expand Down Expand Up @@ -325,6 +327,8 @@ class Overview extends React.Component {
fetchTransformationMappingsUrl={fetchTransformationMappingsUrl}
fetchTransformationMappingsAction={fetchTransformationMappingsAction}
showEditPlanNameModalAction={showEditPlanNameModalAction}
acknowledgeDeniedPlanRequestAction={acknowledgeDeniedPlanRequestAction}
isEditingPlanRequest={isEditingPlanRequest}
/>
) : (
<ShowWizardEmptyState
Expand Down Expand Up @@ -471,7 +475,9 @@ Overview.propTypes = {
serviceTemplatePlaybooks: PropTypes.array,
redirectTo: PropTypes.func.isRequired,
openMappingWizardOnTransitionAction: PropTypes.func,
initialMigrationsFilterSet: PropTypes.bool
initialMigrationsFilterSet: PropTypes.bool,
acknowledgeDeniedPlanRequestAction: PropTypes.func,
isEditingPlanRequest: PropTypes.bool
};

Overview.defaultProps = {
Expand Down
38 changes: 37 additions & 1 deletion app/javascript/react/screens/App/Overview/OverviewActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {
V2V_SCHEDULE_MIGRATION,
V2V_SET_MIGRATIONS_FILTER,
V2V_TOGGLE_SCHEDULE_MIGRATION_MODAL,
SHOW_PLAN_WIZARD_EDIT_MODE
SHOW_PLAN_WIZARD_EDIT_MODE,
V2V_EDIT_PLAN_REQUEST
} from './OverviewConstants';

import { OPEN_V2V_MAPPING_WIZARD_ON_MOUNT } from '../Mappings/MappingsConstants';
Expand Down Expand Up @@ -252,3 +253,38 @@ export const scheduleMigration = payload => dispatch =>
});

export const openMappingWizardOnTransitionAction = () => ({ type: OPEN_V2V_MAPPING_WIZARD_ON_MOUNT });

const _editPlanRequestActionCreator = ({ planRequestUrl, plansUrl, resource }) => dispatch =>
dispatch({
type: V2V_EDIT_PLAN_REQUEST,
payload: new Promise((resolve, reject) =>
API.post(planRequestUrl, { action: 'edit', resource })
.then(response => {
resolve(response);
fetchTransformationPlansAction({
url: plansUrl,
archived: false
})(dispatch);
})
.catch(e => reject(e))
)
});

export const editPlanRequestAction = ({ planRequestUrl, plansUrl, resource }) =>
_editPlanRequestActionCreator({
planRequestUrl: new URI(planRequestUrl).toString(),
plansUrl: new URI(plansUrl).toString(),
resource
});

export const acknowledgeDeniedPlanRequestAction = ({ plansUrl, planRequest }) =>
editPlanRequestAction({
planRequestUrl: planRequest.href,
plansUrl,
resource: {
options: {
...planRequest.options,
denial_acknowledged: true
}
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const V2V_AUTO_SET_MIGRATIONS_FILTER = 'V2V_AUTO_SET_MIGRATIONS_FILTER';
export const V2V_RETRY_MIGRATION = 'V2V_RETRY_MIGRATION';
export const V2V_TOGGLE_SCHEDULE_MIGRATION_MODAL = 'V2V_TOGGLE_SCHEDULE_MIGRATION_MODAL';
export const V2V_SCHEDULE_MIGRATION = 'V2V_SCHEDULE_MIGRATION';
export const V2V_EDIT_PLAN_REQUEST = 'V2V_EDIT_PLAN_REQUEST';
export const SHOW_CONFIRM_MODAL = 'SHOW_CONFIRM_MODAL';
export const HIDE_CONFIRM_MODAL = 'HIDE_CONFIRM_MODAL';
export const ARCHIVE_TRANSFORMATION_PLAN = 'ARCHIVE_TRANSFORMATION_PLAN';
Expand Down
24 changes: 22 additions & 2 deletions app/javascript/react/screens/App/Overview/OverviewReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import {
ARCHIVE_TRANSFORMATION_PLAN,
V2V_TOGGLE_SCHEDULE_MIGRATION_MODAL,
V2V_SCHEDULE_MIGRATION,
SHOW_PLAN_WIZARD_EDIT_MODE
SHOW_PLAN_WIZARD_EDIT_MODE,
V2V_EDIT_PLAN_REQUEST
} from './OverviewConstants';

import { planTransmutation, sufficientProviders } from './helpers';
Expand Down Expand Up @@ -88,7 +89,10 @@ export const initialState = Immutable({
isFetchingServiceTemplatePlaybooks: false,
isRejectedServiceTemplatePlaybooks: false,
errorServiceTemplatePlaybooks: null,
initialMigrationsFilterSet: false
initialMigrationsFilterSet: false,
isEditingPlanRequest: false,
isRejectedEditingPlanRequest: false,
errorEditingPlanRequest: null
});

export default (state = initialState, action) => {
Expand Down Expand Up @@ -304,6 +308,22 @@ export default (state = initialState, action) => {
case V2V_AUTO_SET_MIGRATIONS_FILTER:
return state.set('initialMigrationsFilterSet', true);

case `${V2V_EDIT_PLAN_REQUEST}_PENDING`:
return state
.set('isEditingPlanRequest', true)
.set('isRejectedEditingPlanRequest', false)
.set('errorEditingPlanRequest', null);
case `${V2V_EDIT_PLAN_REQUEST}_FULFILLED`:
return state
.set('isEditingPlanRequest', false)
.set('isRejectedEditingPlanRequest', false)
.set('errorEditingPlanRequest', null);
case `${V2V_EDIT_PLAN_REQUEST}_REJECTED`:
return state
.set('isEditingPlanRequest', false)
.set('isRejectedEditingPlanRequest', true)
.set('errorEditingPlanRequest', action.payload);

default:
return state;
}
Expand Down
17 changes: 14 additions & 3 deletions app/javascript/react/screens/App/Overview/OverviewSelectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ export const activeTransformationPlansFilter = (transformationPlans, planId) =>
}
if (transformationPlan.miq_requests.length > 0) {
const mostRecentRequest = getMostRecentRequest(transformationPlan.miq_requests);
return mostRecentRequest.request_state === 'active' || mostRecentRequest.request_state === 'pending';
return (
mostRecentRequest.request_state === 'active' ||
mostRecentRequest.request_state === 'pending' ||
(mostRecentRequest.approval_state === 'denied' && !mostRecentRequest.options.denial_acknowledged)
);
}
return false;
});
Expand All @@ -19,7 +23,11 @@ export const finishedTransformationPlansFilter = transformationPlans =>
transformationPlans.filter(transformationPlan => {
if (transformationPlan.miq_requests.length > 0) {
const mostRecentRequest = getMostRecentRequest(transformationPlan.miq_requests);
return mostRecentRequest.request_state === 'finished' || mostRecentRequest.request_state === 'failed';
return (
(mostRecentRequest.request_state === 'finished' && mostRecentRequest.approval_state !== 'denied') ||
mostRecentRequest.request_state === 'failed' ||
(mostRecentRequest.approval_state === 'denied' && mostRecentRequest.options.denial_acknowledged)
);
}
return false;
});
Expand All @@ -28,7 +36,10 @@ export const finishedWithErrorTransformationPlansFilter = transformationPlans =>
transformationPlans.filter(transformationPlan => {
if (transformationPlan.miq_requests.length > 0) {
const mostRecentRequest = getMostRecentRequest(transformationPlan.miq_requests);
return mostRecentRequest.request_state === 'finished' && mostRecentRequest.status === 'Error';
return (
(mostRecentRequest.request_state === 'finished' && mostRecentRequest.status === 'Error') ||
mostRecentRequest.approval_state === 'denied'
);
}
return false;
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

exports[`Overview integration test should mount the Overview with mapStateToProps reduced 1`] = `
Object {
"acknowledgeDeniedPlanRequestAction": [Function],
"activeTransformationPlans": Array [],
"addNotificationAction": [Function],
"archiveTransformationPlanAction": [Function],
Expand All @@ -12,6 +13,7 @@ Object {
"datastores": Array [],
"deleteTransformationPlanAction": [Function],
"deleteTransformationPlanUrl": "/api/service_templates",
"editPlanRequestAction": [Function],
"fetchArchivedTransformationPlansUrl": "/api/dummyArchivedTransformationPlans",
"fetchClustersUrl": "/api/dummyClusters",
"fetchDatastoresUrl": "/api/dummyDatastores",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const FinishedTransformationPlans = ({ finishedPlans, loading, migrationsFilter,
const active = migrationsFilter === MIGRATIONS_FILTERS.completed;
const failedPlans = finishedPlans.filter(plan => {
const mostRecentRequest = plan.miq_requests.length > 0 && getMostRecentRequest(plan.miq_requests);
return mostRecentRequest.status === 'Error';
return mostRecentRequest.status === 'Error' || mostRecentRequest.status === 'Denied';
});

const classes = cx('overview-aggregate-card', { 'is-loading': loading, active });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Card, Grid, noop } from 'patternfly-react';

const InProgressCard = ({ title, children, onClick, ...props }) => (
const InProgressCard = ({ title, children, footer, onClick, ...props }) => (
<Grid.Col sm={12} md={6} lg={4}>
<Card
id={`${Array.isArray(title.props.children) ? title.props.children[1] : title.props.children}-progress-card`}
Expand All @@ -12,19 +12,22 @@ const InProgressCard = ({ title, children, onClick, ...props }) => (
>
<Card.Heading>{title}</Card.Heading>
<Card.Body>{children}</Card.Body>
{footer}
</Card>
</Grid.Col>
);

InProgressCard.propTypes = {
title: PropTypes.node,
children: PropTypes.node,
footer: PropTypes.node,
onClick: PropTypes.func
};

InProgressCard.defaultProps = {
title: '',
children: null,
footer: null,
onClick: noop
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ class Migrations extends React.Component {
showPlanWizardEditModeAction,
fetchTransformationMappingsUrl,
fetchTransformationMappingsAction,
showEditPlanNameModalAction
showEditPlanNameModalAction,
acknowledgeDeniedPlanRequestAction,
isEditingPlanRequest,
setMigrationsFilterAction
} = this.props;

const plansExist = transformationPlans.length > 0 || archivedTransformationPlans.length > 0;
Expand Down Expand Up @@ -137,6 +140,10 @@ class Migrations extends React.Component {
reloadCard={reloadCard}
loading={isCreatingTransformationPlanRequest !== null}
redirectTo={redirectTo}
fetchTransformationPlansUrl={fetchTransformationPlansUrl}
acknowledgeDeniedPlanRequestAction={acknowledgeDeniedPlanRequestAction}
isEditingPlanRequest={isEditingPlanRequest}
setMigrationsFilterAction={setMigrationsFilterAction}
/>
)}
{activeFilter === MIGRATIONS_FILTERS.completed && (
Expand Down Expand Up @@ -227,7 +234,9 @@ Migrations.propTypes = {
showPlanWizardEditModeAction: PropTypes.func,
fetchTransformationMappingsAction: PropTypes.func,
fetchTransformationMappingsUrl: PropTypes.string,
showEditPlanNameModalAction: PropTypes.func
showEditPlanNameModalAction: PropTypes.func,
acknowledgeDeniedPlanRequestAction: PropTypes.func,
isEditingPlanRequest: PropTypes.bool
};
Migrations.defaultProps = {
transformationPlans: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const MigrationsCompletedList = ({
requestsOfAssociatedPlan.length > 0 && getMostRecentRequest(requestsOfAssociatedPlan);

const failed = mostRecentRequest && mostRecentRequest.status === 'Error';
const denied = mostRecentRequest && mostRecentRequest.status === 'Denied';

const tasks = {};
let tasksOfPlan = {};
Expand Down Expand Up @@ -209,7 +210,7 @@ const MigrationsCompletedList = ({
) : (
<ListView.Icon
type="pf"
name={failed ? 'error-circle-o' : 'ok'}
name={failed || denied ? 'error-circle-o' : 'ok'}
size="md"
style={{
width: 'inherit',
Expand Down Expand Up @@ -238,10 +239,12 @@ const MigrationsCompletedList = ({
<a href="/migration/mappings#">{plan.infraMappingName}</a>
</ListView.InfoItem>
),
<ListView.InfoItem key={`${plan.id}-elapsed`}>
<ListView.Icon type="fa" size="lg" name="clock-o" />
{elapsedTime}
</ListView.InfoItem>,
!denied ? (
<ListView.InfoItem key={`${plan.id}-elapsed`}>
<ListView.Icon type="fa" size="lg" name="clock-o" />
{elapsedTime}
</ListView.InfoItem>
) : null,
migrationScheduled && !staleMigrationSchedule && !migrationStarting ? (
<ListView.InfoItem key={`${plan.id}-scheduledTime`} style={{ textAlign: 'left' }}>
<Icon type="fa" name="clock-o" />
Expand All @@ -259,7 +262,7 @@ const MigrationsCompletedList = ({
actions={
<div>
{!archived &&
failed && (
(failed || denied) && (
<React.Fragment>
{showInitialScheduleButton && scheduleButtons}
<Button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import numeral from 'numeral';
import { EmptyState, Icon, OverlayTrigger, Popover, Tooltip, UtilizationBar, Spinner } from 'patternfly-react';
import {
EmptyState,
Icon,
OverlayTrigger,
Popover,
Tooltip,
UtilizationBar,
Spinner,
Card,
Button
} from 'patternfly-react';
import InProgressCard from './InProgressCard';
import InProgressWithDetailCard from './InProgressWithDetailCard';
import TickingIsoElapsedTime from '../../../../../../components/dates/TickingIsoElapsedTime';
import getMostRecentRequest from '../../../common/getMostRecentRequest';
import getMostRecentVMTasksFromRequests from './helpers/getMostRecentVMTasksFromRequests';
import getPlaybookName from './helpers/getPlaybookName';
import { PLAN_JOB_STATES } from '../../../../../../data/models/plans';
import { DOCS_URL_CONFIGURE_CONVERSION_HOSTS } from '../../../Plan/PlanConstants';
import { MIGRATIONS_FILTERS } from '../../OverviewConstants';

const MigrationsInProgressCard = ({
plan,
serviceTemplatePlaybooks,
allRequestsWithTasks,
reloadCard,
handleClick
handleClick,
fetchTransformationPlansUrl,
acknowledgeDeniedPlanRequestAction,
isEditingPlanRequest,
setMigrationsFilterAction
}) => {
const requestsOfAssociatedPlan = allRequestsWithTasks.filter(request => request.source_id === plan.id);
const mostRecentRequest = requestsOfAssociatedPlan.length > 0 && getMostRecentRequest(requestsOfAssociatedPlan);
Expand All @@ -36,6 +52,41 @@ const MigrationsInProgressCard = ({
);
}

if (mostRecentRequest.approval_state === 'denied') {
const cardEmptyState = (
<EmptyState>
<EmptyState.Icon type="pf" name="error-circle-o" />
<EmptyState.Info style={{ marginTop: 10 }}>
{__('Unable to migrate VMs because no conversion host was configured at the time of the attempted migration.') /* prettier-ignore */}{' '}
<a href={DOCS_URL_CONFIGURE_CONVERSION_HOSTS} target="_blank" rel="noopener noreferrer">
{__('See the product documentation for information on configuring conversion hosts.')}
</a>
</EmptyState.Info>
</EmptyState>
);
const cardFooter = (
<Card.Footer style={{ position: 'relative', top: '-2px' }}>
<Button
style={{ position: 'relative', top: '-5px' }}
onClick={() =>
acknowledgeDeniedPlanRequestAction({
plansUrl: fetchTransformationPlansUrl,
planRequest: mostRecentRequest
}).then(() => setMigrationsFilterAction(MIGRATIONS_FILTERS.completed))
}
disabled={isEditingPlanRequest}
>
{__('Cancel Migration')}
</Button>
</Card.Footer>
);
return (
<InProgressCard title={<h3 className="card-pf-title">{plan.name}</h3>} footer={cardFooter}>
{cardEmptyState}
</InProgressCard>
);
}

// UX business rule: reflect failed immediately if any single task has failed
// in the most recent request
let failed = false;
Expand Down Expand Up @@ -243,7 +294,11 @@ MigrationsInProgressCard.propTypes = {
serviceTemplatePlaybooks: PropTypes.array,
allRequestsWithTasks: PropTypes.array,
reloadCard: PropTypes.bool,
handleClick: PropTypes.func
handleClick: PropTypes.func,
fetchTransformationPlansUrl: PropTypes.string,
acknowledgeDeniedPlanRequestAction: PropTypes.func,
isEditingPlanRequest: PropTypes.bool,
setMigrationsFilterAction: PropTypes.func
};

export default MigrationsInProgressCard;
Loading

0 comments on commit 9b78d09

Please sign in to comment.