From 9796dfd1aff380ce17c12c1439f1b3a794dbb853 Mon Sep 17 00:00:00 2001 From: Milan Zazrivec Date: Wed, 23 Oct 2019 18:35:45 +0200 Subject: [PATCH 1/2] Unify Schedule, Migrate & Retry buttons into one --- .../components/Migrations/Migrations.js | 1 + .../Migrations/MigrationsCompletedList.js | 17 +-- .../Migrations/MigrationsNotStartedList.js | 15 +-- .../Migrations/ScheduleMigrationButton.js | 28 ++-- .../ScheduleMigrationModal.js | 61 ++++++--- .../ScheduleMigrationModalBody.js | 125 +++++++++++++----- 6 files changed, 160 insertions(+), 87 deletions(-) diff --git a/app/javascript/react/screens/App/Overview/components/Migrations/Migrations.js b/app/javascript/react/screens/App/Overview/components/Migrations/Migrations.js index 9d4b618161..01eb0a92f7 100644 --- a/app/javascript/react/screens/App/Overview/components/Migrations/Migrations.js +++ b/app/javascript/react/screens/App/Overview/components/Migrations/Migrations.js @@ -159,6 +159,7 @@ class Migrations extends React.Component { )} {activeFilter === MIGRATIONS_FILTERS.completed && ( ( @@ -255,15 +256,6 @@ const MigrationsCompletedList = ({ isMissingMapping={isMissingMapping} /> - @@ -348,6 +340,7 @@ const MigrationsCompletedList = ({ scheduleMigrationModal={scheduleMigrationModal} scheduleMigrationPlan={scheduleMigrationPlan} scheduleMigration={scheduleMigration} + migrateClick={migrateClick} fetchTransformationPlansAction={fetchTransformationPlansAction} fetchTransformationPlansUrl={fetchTransformationPlansUrl} /> @@ -355,6 +348,7 @@ const MigrationsCompletedList = ({ ); MigrationsCompletedList.propTypes = { + migrateClick: PropTypes.func, finishedTransformationPlans: PropTypes.array, allRequestsWithTasks: PropTypes.array, retryClick: PropTypes.func, @@ -380,6 +374,7 @@ MigrationsCompletedList.propTypes = { showEditPlanNameModalAction: PropTypes.func }; MigrationsCompletedList.defaultProps = { + migrateClick: noop, finishedTransformationPlans: [], retryClick: noop, loading: false diff --git a/app/javascript/react/screens/App/Overview/components/Migrations/MigrationsNotStartedList.js b/app/javascript/react/screens/App/Overview/components/Migrations/MigrationsNotStartedList.js index bb5d9c626a..6c79e4ca6e 100644 --- a/app/javascript/react/screens/App/Overview/components/Migrations/MigrationsNotStartedList.js +++ b/app/javascript/react/screens/App/Overview/components/Migrations/MigrationsNotStartedList.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { noop, Button, Grid, Spinner, Toolbar, DropdownKebab, MenuItem } from 'patternfly-react'; +import { noop, Grid, Spinner, Toolbar, DropdownKebab, MenuItem } from 'patternfly-react'; import EllipsisWithTooltip from 'react-ellipsis-with-tooltip'; import ShowWizardEmptyState from '../../../common/ShowWizardEmptyState/ShowWizardEmptyState'; import ScheduleMigrationModal from '../ScheduleMigrationModal/ScheduleMigrationModal'; @@ -86,18 +86,6 @@ const MigrationsNotStartedList = ({ isMissingMapping={isMissingMapping} /> )} - diff --git a/app/javascript/react/screens/App/Overview/components/Migrations/ScheduleMigrationButton.js b/app/javascript/react/screens/App/Overview/components/Migrations/ScheduleMigrationButton.js index d9686c5136..340bf9e18e 100644 --- a/app/javascript/react/screens/App/Overview/components/Migrations/ScheduleMigrationButton.js +++ b/app/javascript/react/screens/App/Overview/components/Migrations/ScheduleMigrationButton.js @@ -2,18 +2,22 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button } from 'patternfly-react'; -const ScheduleMigrationButton = ({ loading, toggleScheduleMigrationModal, plan, isMissingMapping }) => ( - -); +const ScheduleMigrationButton = ({ loading, toggleScheduleMigrationModal, plan, isMissingMapping }) => { + const retry = plan.status === 'Error' || plan.status === 'Denied'; + + return ( + + ); +}; ScheduleMigrationButton.propTypes = { loading: PropTypes.string, diff --git a/app/javascript/react/screens/App/Overview/components/ScheduleMigrationModal/ScheduleMigrationModal.js b/app/javascript/react/screens/App/Overview/components/ScheduleMigrationModal/ScheduleMigrationModal.js index b67c292a65..2be909c553 100644 --- a/app/javascript/react/screens/App/Overview/components/ScheduleMigrationModal/ScheduleMigrationModal.js +++ b/app/javascript/react/screens/App/Overview/components/ScheduleMigrationModal/ScheduleMigrationModal.js @@ -5,7 +5,17 @@ import ScheduleMigrationModalBody from './ScheduleMigrationModalBody'; import getPlanScheduleInfo from '../Migrations/helpers/getPlanScheduleInfo'; class ScheduleMigrationModal extends React.Component { - state = { dateTimeInput: '' }; + constructor(props) { + super(props); + + const { scheduleMigrationPlan } = this.props; + const { migrationScheduled } = getPlanScheduleInfo(scheduleMigrationPlan) || ''; + + this.state = { + dateTimeInput: migrationScheduled ? new Date(migrationScheduled) : null, + startMigrationNow: null + }; + } render() { const { @@ -14,28 +24,39 @@ class ScheduleMigrationModal extends React.Component { scheduleMigrationPlan, scheduleMigration, fetchTransformationPlansAction, - fetchTransformationPlansUrl + fetchTransformationPlansUrl, + migrateClick } = this.props; - const { migrationScheduled } = getPlanScheduleInfo(scheduleMigrationPlan); - - const handleChange = event => { + const handleDatepickerChange = event => { this.setState({ dateTimeInput: event }); }; + const startMigrationNowHandler = event => { + this.setState({ startMigrationNow: event }); + }; + const modalClose = () => { toggleScheduleMigrationModal(); - handleChange(); }; + const modalTitle = plan => + plan != null && (plan.status === 'Error' || plan.status === 'Denied') + ? __('Schedule Retry') + : __('Schedule Migration'); + return ( - {__('Schedule Migration Plan')} + {modalTitle(scheduleMigrationPlan)} - + ); }; ScheduleMigrationButton.propTypes = { - loading: PropTypes.string, + loading: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), toggleScheduleMigrationModal: PropTypes.func, plan: PropTypes.object, isMissingMapping: PropTypes.bool diff --git a/app/javascript/react/screens/App/Overview/components/Migrations/__test__/MigrationsNotStartedList.test.js b/app/javascript/react/screens/App/Overview/components/Migrations/__test__/MigrationsNotStartedList.test.js index b52bfd3d13..d6b80ed04a 100644 --- a/app/javascript/react/screens/App/Overview/components/Migrations/__test__/MigrationsNotStartedList.test.js +++ b/app/javascript/react/screens/App/Overview/components/Migrations/__test__/MigrationsNotStartedList.test.js @@ -8,15 +8,15 @@ import ListViewTableRow from '../../../../common/ListViewTable/ListViewTableRow' const { resources: plans } = transformationPlans; const [notStartedPlan] = plans; -let migrateClick; +let scheduleMigrationNow; let redirectTo; let wrapper; beforeEach(() => { - migrateClick = jest.fn(); + scheduleMigrationNow = jest.fn(); redirectTo = jest.fn(); wrapper = mount( { +test.skip('clicking on the Migrate button fires scheduleMigrationNow with the correct API endpoint', () => { const e = { stopPropagation: jest.fn() }; @@ -42,5 +42,5 @@ test.skip('clicking on the Migrate button fires migrateClick with the correct AP .prop('actions') .props.children.props.onClick(e); - expect(migrateClick).toHaveBeenLastCalledWith(`/api/service_templates/${notStartedPlan.id}`); + expect(scheduleMigrationNow).toHaveBeenLastCalledWith(`/api/service_templates/${notStartedPlan.id}`); }); diff --git a/app/javascript/react/screens/App/Overview/components/Migrations/__test__/__snapshots__/MigrationInProgressListItem.test.js.snap b/app/javascript/react/screens/App/Overview/components/Migrations/__test__/__snapshots__/MigrationInProgressListItem.test.js.snap index 6fcaac06ee..de8667b6ae 100644 --- a/app/javascript/react/screens/App/Overview/components/Migrations/__test__/__snapshots__/MigrationInProgressListItem.test.js.snap +++ b/app/javascript/react/screens/App/Overview/components/Migrations/__test__/__snapshots__/MigrationInProgressListItem.test.js.snap @@ -37,6 +37,73 @@ exports[`if there are no conversion hosts available renders an error view 1`] = Waiting for an available conversion host. You can continue waiting or go to the Migration Settings page to increase the number of migrations per host. , + ", + "options": Object { + "cart_state": "ordered", + "delivered_on": "2018-04-06T12:49:30Z", + "dialog": null, + "initiator": null, + "requester_group": "EvmGroup-super_administrator", + "src_id": "60", + "workflow_settings": Object { + "resource_action_id": "2507", + }, + }, + "process": true, + "request_state": "active", + "request_type": "transformation_plan", + "requester_id": "1", + "requester_name": "Administrator", + "service_order_id": "91", + "source_id": "60", + "source_type": "ServiceTemplate", + "status": "Ok", + "tenant_id": "1", + "type": "ServiceTemplateTransformationPlanRequest", + "updated_on": "2018-04-06T12:31:30Z", + "userid": "admin", + }, + ], + "name": "Migration Plan F-0", + "options": Object { + "config_info": Object { + "actions": Array [ + Object { + "vm_id": "1", + }, + Object { + "vm_id": "3", + }, + ], + "transformation_mapping_id": "1", + }, + }, + "scheduleTime": null, + "status": "Ok", + "transformation_mapping": Object { + "transformation_mapping_items": Array [], + }, + } + } + />, ] } numFailedVms={0} @@ -146,6 +213,73 @@ exports[`when the request is denied renders an error view 1`] = ` See the product documentation for information on configuring conversion hosts. , + ", + "options": Object { + "cart_state": "ordered", + "delivered_on": "2018-04-06T12:49:30Z", + "dialog": null, + "initiator": null, + "requester_group": "EvmGroup-super_administrator", + "src_id": "60", + "workflow_settings": Object { + "resource_action_id": "2507", + }, + }, + "process": true, + "request_state": "active", + "request_type": "transformation_plan", + "requester_id": "1", + "requester_name": "Administrator", + "service_order_id": "91", + "source_id": "80", + "source_type": "ServiceTemplate", + "status": "Ok", + "tenant_id": "1", + "updated_on": "2018-04-06T12:31:30Z", + "userid": "admin", + }, + ], + "name": "Migration Plan H-0", + "options": Object { + "config_info": Object { + "actions": Array [ + Object { + "vm_id": "1", + }, + Object { + "vm_id": "3", + }, + ], + "transformation_mapping_id": "1", + }, + }, + "scheduleTime": null, + "status": "Ok", + "transformation_mapping": Object { + "transformation_mapping_items": Array [], + }, + } + } + />, ] } numFailedVms={0} diff --git a/app/javascript/react/screens/App/Overview/components/Migrations/helpers/getPlanScheduleInfo.js b/app/javascript/react/screens/App/Overview/components/Migrations/helpers/getPlanScheduleInfo.js index 926f79c201..ae556a9797 100644 --- a/app/javascript/react/screens/App/Overview/components/Migrations/helpers/getPlanScheduleInfo.js +++ b/app/javascript/react/screens/App/Overview/components/Migrations/helpers/getPlanScheduleInfo.js @@ -3,11 +3,19 @@ const getPlanScheduleInfo = plan => { const staleMigrationSchedule = new Date(migrationScheduled).getTime() < Date.now(); const migrationStarting = staleMigrationSchedule && new Date(migrationScheduled).getTime() > Date.now() - 120000; const showInitialScheduleButton = staleMigrationSchedule && !migrationStarting; + const migrationCutover = + (plan && + plan.options && + plan.options.config_info && + plan.options.config_info.warm_migration && + plan.options.config_info.warm_migration_cutover_datetime) || + 0; return { migrationScheduled, staleMigrationSchedule, migrationStarting, - showInitialScheduleButton + showInitialScheduleButton, + migrationCutover }; }; diff --git a/app/javascript/react/screens/App/Overview/components/Migrations/helpers/isPlanWarmMigration.js b/app/javascript/react/screens/App/Overview/components/Migrations/helpers/isPlanWarmMigration.js new file mode 100644 index 0000000000..02223da87c --- /dev/null +++ b/app/javascript/react/screens/App/Overview/components/Migrations/helpers/isPlanWarmMigration.js @@ -0,0 +1,4 @@ +const isPlanWarmMigration = plan => + plan != null && plan.options && plan.options.config_info && plan.options.config_info.warm_migration; + +export default isPlanWarmMigration; diff --git a/app/javascript/react/screens/App/Overview/components/ScheduleMigrationModal/ScheduleMigrationModal.js b/app/javascript/react/screens/App/Overview/components/ScheduleMigrationModal/ScheduleMigrationModal.js index 2be909c553..32916131ce 100644 --- a/app/javascript/react/screens/App/Overview/components/ScheduleMigrationModal/ScheduleMigrationModal.js +++ b/app/javascript/react/screens/App/Overview/components/ScheduleMigrationModal/ScheduleMigrationModal.js @@ -1,18 +1,17 @@ +import moment from 'moment'; import React from 'react'; import PropTypes from 'prop-types'; import { Button, Modal } from 'patternfly-react'; import ScheduleMigrationModalBody from './ScheduleMigrationModalBody'; import getPlanScheduleInfo from '../Migrations/helpers/getPlanScheduleInfo'; +import isPlanWarmMigration from '../Migrations/helpers/isPlanWarmMigration'; class ScheduleMigrationModal extends React.Component { constructor(props) { super(props); - const { scheduleMigrationPlan } = this.props; - const { migrationScheduled } = getPlanScheduleInfo(scheduleMigrationPlan) || ''; - this.state = { - dateTimeInput: migrationScheduled ? new Date(migrationScheduled) : null, + dateTimeInput: null, startMigrationNow: null }; } @@ -25,14 +24,15 @@ class ScheduleMigrationModal extends React.Component { scheduleMigration, fetchTransformationPlansAction, fetchTransformationPlansUrl, - migrateClick + scheduleMigrationNow, + scheduleCutover } = this.props; const handleDatepickerChange = event => { this.setState({ dateTimeInput: event }); }; - const startMigrationNowHandler = event => { + const setScheduleMode = event => { this.setState({ startMigrationNow: event }); }; @@ -40,10 +40,34 @@ class ScheduleMigrationModal extends React.Component { toggleScheduleMigrationModal(); }; - const modalTitle = plan => - plan != null && (plan.status === 'Error' || plan.status === 'Denied') - ? __('Schedule Retry') - : __('Schedule Migration'); + const modalMode = plan => { + let mode = 'migration'; + if (isPlanWarmMigration(plan) && plan.status === 'Ok') { + mode = 'cutover'; + } else if (plan != null && (plan.status === 'Error' || plan.status === 'Denied')) { + mode = 'retry'; + } + return mode; + }; + + const MODAL_STRINGS = { + migration: [ + __('Schedule Migration'), + __('Start migration immediately'), + __('Select date and time for the start of the migration') + ], + cutover: [__('Schedule Cutover'), __('Start cutover immediately'), __('Select date and time to start cutover')], + retry: [__('Schedule Retry'), __('Retry migration immediately'), __('Select date and time to retry migration')] + }; + + const modalTitle = plan => MODAL_STRINGS[modalMode(plan)][0]; + + const modalBodyStrings = plan => [MODAL_STRINGS[modalMode(plan)][1], MODAL_STRINGS[modalMode(plan)][2]]; + + const { migrationScheduled, migrationCutover } = getPlanScheduleInfo(scheduleMigrationPlan); + const defaultDate = isPlanWarmMigration(scheduleMigrationPlan) + ? new Date(migrationCutover) + : new Date(migrationScheduled); return ( @@ -54,8 +78,11 @@ class ScheduleMigrationModal extends React.Component { @@ -65,8 +92,25 @@ class ScheduleMigrationModal extends React.Component {