From bdd943162e9c49297df655043a9dfb340adb308c Mon Sep 17 00:00:00 2001 From: Mike Ro Date: Thu, 7 Jun 2018 16:23:47 -0400 Subject: [PATCH] Add Advanced Options Step to Plan Wizard * Add VM/Playbook selection table * Custom reactabular formatters to enable dual selection columns * Extract some common helpers shared with VMStep table * Make modifications to BootstrapSelect for this use case * Confirm modal for case when a user selects a playbook, but does not select any VMs on which to run service * Form data is reset if user navigates back from Advanced Options Step to the VM Selection Step * Add schema validator for ServiceTemplateAnsiblePlaybook --- app/javascript/components/index.js | 9 + .../models/serviceTemplateAnsiblePlaybook.js | 8 + .../models/serviceTemplateAnsiblePlaybooks.js | 6 + .../react/screens/App/Overview/Overview.scss | 1 + .../MappingWizardDatastoresStep.js | 3 + .../MappingWizardNetworksStep.js | 3 + .../Overview/screens/PlanWizard/PlanWizard.js | 78 ++- .../screens/PlanWizard/PlanWizard.scss | 9 + .../screens/PlanWizard/PlanWizardActions.js | 15 +- .../screens/PlanWizard/PlanWizardBody.js | 12 +- .../screens/PlanWizard/PlanWizardSelectors.js | 3 +- .../__snapshots__/index.test.js.snap | 6 +- .../PlanWizardAdvancedOptionsStep.fixtures.js | 100 ++++ .../PlanWizardAdvancedOptionsStep.js | 107 ++++ .../PlanWizardAdvancedOptionsStep.scss | 28 + .../PlanWizardAdvancedOptionsStepActions.js | 28 + .../PlanWizardAdvancedOptionsStepConstants.js | 18 + .../PlanWizardAdvancedOptionsStepReducer.js | 42 ++ .../PlanWizardAdvancedOptionsStepSelectors.js | 1 + ...PlanWizardAdvancedOptionsStepValidators.js | 6 + .../PlanWizardAdvancedOptionsStepTable.js | 518 ++++++++++++++++++ .../vmSelectionCellFormatter.js | 44 ++ .../vmSelectionHeaderCellFormatter.js | 48 ++ .../PlanWizardAdvancedOptionsStep/index.js | 49 ++ .../PlanWizardGeneralStep.js | 3 + .../PlanWizardScheduleStep.js} | 6 +- .../index.js | 4 +- .../components/PlanWizardVMStepTable.js | 6 +- .../components/common/CustomToolbarFind.js | 104 ++++ .../PlanWizard/components/common/rowFilter.js | 29 + .../components/common/searchFilter.js | 23 + .../Overview/screens/PlanWizard/helpers.js | 16 +- .../App/Overview/screens/PlanWizard/index.js | 6 +- .../App/common/forms/BootstrapSelect.js | 31 +- app/javascript/redux/reducers/index.js | 2 + 35 files changed, 1325 insertions(+), 47 deletions(-) create mode 100644 app/javascript/data/models/serviceTemplateAnsiblePlaybook.js create mode 100644 app/javascript/data/models/serviceTemplateAnsiblePlaybooks.js create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizard.scss create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStep.fixtures.js create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStep.js create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStep.scss create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepActions.js create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepConstants.js create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepReducer.js create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepSelectors.js create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepValidators.js create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/components/PlanWizardAdvancedOptionsStepTable/PlanWizardAdvancedOptionsStepTable.js create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/components/PlanWizardAdvancedOptionsStepTable/vmSelectionCellFormatter.js create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/components/PlanWizardAdvancedOptionsStepTable/vmSelectionHeaderCellFormatter.js create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/index.js rename app/javascript/react/screens/App/Overview/screens/PlanWizard/components/{PlanWizardOptionsStep/PlanWizardOptionsStep.js => PlanWizardScheduleStep/PlanWizardScheduleStep.js} (87%) rename app/javascript/react/screens/App/Overview/screens/PlanWizard/components/{PlanWizardOptionsStep => PlanWizardScheduleStep}/index.js (55%) create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/components/common/CustomToolbarFind.js create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/components/common/rowFilter.js create mode 100644 app/javascript/react/screens/App/Overview/screens/PlanWizard/components/common/searchFilter.js diff --git a/app/javascript/components/index.js b/app/javascript/components/index.js index c05c893707..f6aa22164f 100644 --- a/app/javascript/components/index.js +++ b/app/javascript/components/index.js @@ -7,6 +7,7 @@ import MappingWizardResultsStepContainer from '../react/screens/App/Overview/scr import PlanWizardVMStepContainer from '../react/screens/App/Overview/screens/PlanWizard/components/PlanWizardVMStep'; import PlanWizardResultsStepContainer from '../react/screens/App/Overview/screens/PlanWizard/components/PlanWizardResultsStep'; import PlanWizardContainer from '../react/screens/App/Overview/screens/PlanWizard'; +import PlanWizardAdvancedOptionsStepContainer from '../react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep'; import OverviewContainer from '../react/screens/App/Overview'; import PlanContainer from '../react/screens/App/Plan'; @@ -84,6 +85,14 @@ export const coreComponents = [ }, store: true }, + { + name: 'PlanWizardAdvancedOptionsStepContainer', + type: PlanWizardAdvancedOptionsStepContainer, + data: { + fetchPlaybooksUrl: "/api/service_templates/?filter[]=type='ServiceTemplateAnsiblePlaybook'&expand=resources" + }, + store: true + }, { name: 'OverviewContainer', type: OverviewContainer, diff --git a/app/javascript/data/models/serviceTemplateAnsiblePlaybook.js b/app/javascript/data/models/serviceTemplateAnsiblePlaybook.js new file mode 100644 index 0000000000..c29c9b48b3 --- /dev/null +++ b/app/javascript/data/models/serviceTemplateAnsiblePlaybook.js @@ -0,0 +1,8 @@ +import { string, object } from 'yup'; + +export const playbookSchema = object().shape({ + href: string().required(), + id: string().required(), + name: string().required(), + description: string().nullable() +}); diff --git a/app/javascript/data/models/serviceTemplateAnsiblePlaybooks.js b/app/javascript/data/models/serviceTemplateAnsiblePlaybooks.js new file mode 100644 index 0000000000..f8c566d1cf --- /dev/null +++ b/app/javascript/data/models/serviceTemplateAnsiblePlaybooks.js @@ -0,0 +1,6 @@ +import { array } from 'yup'; +import { playbookSchema } from './serviceTemplateAnsiblePlaybook'; + +export const playbooksSchema = array() + .of(playbookSchema) + .nullable(); diff --git a/app/javascript/react/screens/App/Overview/Overview.scss b/app/javascript/react/screens/App/Overview/Overview.scss index 36a6a5a5da..8a5b2ac195 100644 --- a/app/javascript/react/screens/App/Overview/Overview.scss +++ b/app/javascript/react/screens/App/Overview/Overview.scss @@ -1,4 +1,5 @@ @import './screens/MappingWizard/MappingWizard.scss'; +@import './screens/PlanWizard/PlanWizard.scss'; @import './screens/PlanWizard/components/PlanWizardResultsStep/PlanWizardResultsStep.scss'; @import 'components/InfrastructureMappingsList/InfrastructureMappingsList.scss'; @import 'components/Migrations/Migrations.scss'; diff --git a/app/javascript/react/screens/App/Overview/screens/MappingWizard/components/MappingWizardDatastoresStep/MappingWizardDatastoresStep.js b/app/javascript/react/screens/App/Overview/screens/MappingWizard/components/MappingWizardDatastoresStep/MappingWizardDatastoresStep.js index 2c2e67b171..df34b0480b 100644 --- a/app/javascript/react/screens/App/Overview/screens/MappingWizard/components/MappingWizardDatastoresStep/MappingWizardDatastoresStep.js +++ b/app/javascript/react/screens/App/Overview/screens/MappingWizard/components/MappingWizardDatastoresStep/MappingWizardDatastoresStep.js @@ -111,6 +111,9 @@ class MappingWizardDatastoresStep extends React.Component { choose_text={`<${__('Select a source cluster')}>`} render_within_form="true" form_name={form} + inline_label + labelWidth={6} + controlWidth={4} /> `} render_within_form="true" form_name={form} + inline_label + labelWidth={6} + controlWidth={4} /> { - const { resetVmStepAction } = this.props; + const { resetVmStepAction, resetAdvancedOptionsStepAction } = this.props; const { activeStepIndex } = this.state; if (activeStepIndex === 1) { // reset all vm step values if going back from that step resetVmStepAction(); + } else if (activeStepIndex === 2) { + resetAdvancedOptionsStepAction(); } this.setState({ activeStepIndex: Math.max(activeStepIndex - 1, 0) }); }; @@ -26,10 +34,13 @@ class PlanWizard extends React.Component { const { planWizardGeneralStep, planWizardVMStep, - planWizardOptionsStep, + planWizardAdvancedOptionsStep, + planWizardScheduleStep, setPlansBodyAction, setPlanScheduleAction, setMigrationsFilterAction, + showConfirmModalAction, + hideConfirmModalAction, showAlertAction, hideAlertAction } = this.props; @@ -42,22 +53,48 @@ class PlanWizard extends React.Component { hideAlertAction(); } - if (activeStepIndex === 2) { - const plansBody = createMigrationPlans(planWizardGeneralStep, planWizardVMStep); - - setPlanScheduleAction(planWizardOptionsStep.values.migration_plan_choice_radio); + if ( + activeStepIndex === 2 && + ((planWizardAdvancedOptionsStep.values.preMigrationPlaybook && + planWizardAdvancedOptionsStep.values.playbookVms.preMigration.length === 0) || + (planWizardAdvancedOptionsStep.values.postMigrationPlaybook && + planWizardAdvancedOptionsStep.values.playbookVms.postMigration.length === 0)) + ) { + const onConfirm = () => { + this.setState({ activeStepIndex: 3 }); + hideConfirmModalAction(); + }; + + showConfirmModalAction({ + title: __('No VMs Selected'), + body: __( + "You've selected a pre-migration or post-migration playbook service but no VMs on which to run the playbook service. Are you sure you want to continue?" + ), + icon: , + confirmButtonLabel: __('Continue'), + dialogClassName: 'plan-wizard-confirm-modal', + backdropClassName: 'plan-wizard-confirm-backdrop', + onConfirm + }); + } else if (activeStepIndex === 3) { + const plansBody = createMigrationPlans(planWizardGeneralStep, planWizardVMStep, planWizardAdvancedOptionsStep); + + setPlanScheduleAction(planWizardScheduleStep.values.migration_plan_choice_radio); setPlansBodyAction(plansBody); - if (planWizardOptionsStep.values.migration_plan_choice_radio === 'migration_plan_now') { + if (planWizardScheduleStep.values.migration_plan_choice_radio === 'migration_plan_now') { setMigrationsFilterAction(MIGRATIONS_FILTERS.inProgress); - } else if (planWizardOptionsStep.values.migration_plan_choice_radio === 'migration_plan_later') { + } else if (planWizardScheduleStep.values.migration_plan_choice_radio === 'migration_plan_later') { setMigrationsFilterAction(MIGRATIONS_FILTERS.notStarted); } + this.setState({ + activeStepIndex: Math.min(activeStepIndex + 1, planWizardSteps.length - 1) + }); + } else { + this.setState({ + activeStepIndex: Math.min(activeStepIndex + 1, planWizardSteps.length - 1) + }); } - - this.setState({ - activeStepIndex: Math.min(activeStepIndex + 1, planWizardSteps.length - 1) - }); }; goToStep = activeStepIndex => { @@ -71,7 +108,7 @@ class PlanWizard extends React.Component { planWizardExitedAction, planWizardGeneralStep, planWizardVMStep, - planWizardOptionsStep, + planWizardScheduleStep, alertText, alertType, hideAlertAction @@ -106,7 +143,7 @@ class PlanWizard extends React.Component { plansBody={plansBody} planWizardGeneralStep={planWizardGeneralStep} planWizardVMStep={planWizardVMStep} - planWizardOptionsStep={planWizardOptionsStep} + planWizardScheduleStep={planWizardScheduleStep} alertText={alertText} alertType={alertType} hideAlertAction={hideAlertAction} @@ -126,7 +163,7 @@ class PlanWizard extends React.Component { onClick={onFinalStep ? hidePlanWizardAction : this.nextStep} disabled={disableNextStep} > - {onFinalStep ? __('Close') : activeStepIndex === 2 ? __('Create') : __('Next')} + {onFinalStep ? __('Close') : activeStepIndex === 3 ? __('Create') : __('Next')} @@ -140,11 +177,15 @@ PlanWizard.propTypes = { planWizardExitedAction: PropTypes.func, planWizardGeneralStep: PropTypes.object, planWizardVMStep: PropTypes.object, - planWizardOptionsStep: PropTypes.object, + planWizardAdvancedOptionsStep: PropTypes.object, // eslint-disable-line react/no-unused-prop-types + planWizardScheduleStep: PropTypes.object, setPlansBodyAction: PropTypes.func, setPlanScheduleAction: PropTypes.func, resetVmStepAction: PropTypes.func, setMigrationsFilterAction: PropTypes.func, + showConfirmModalAction: PropTypes.func, + hideConfirmModalAction: PropTypes.func, + resetAdvancedOptionsStepAction: PropTypes.func, showAlertAction: PropTypes.func, hideAlertAction: PropTypes.func, alertText: PropTypes.string, @@ -156,7 +197,8 @@ PlanWizard.defaultProps = { planWizardExitedAction: noop, planWizardGeneralStep: {}, planWizardVMStep: {}, - planWizardOptionsStep: {}, + planWizardAdvancedOptionsStep: {}, + planWizardScheduleStep: {}, setPlansBodyAction: noop, setPlanScheduleAction: noop, resetVmStepAction: noop, diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizard.scss b/app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizard.scss new file mode 100644 index 0000000000..0ca307b911 --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizard.scss @@ -0,0 +1,9 @@ +@import './components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStep.scss'; + +.plan-wizard-confirm-backdrop { + z-index: 1050; +} + +.plan-wizard-confirm-modal { + margin-top: 140px; +} diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizardActions.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizardActions.js index 9596d37348..0b0bb611b6 100644 --- a/app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizardActions.js +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizardActions.js @@ -6,7 +6,7 @@ import { V2V_PLAN_WIZARD_SHOW_ALERT, V2V_PLAN_WIZARD_HIDE_ALERT } from './PlanWizardConstants'; - +import { RESET_V2V_ADVANCED_OPTIONS_STEP_VMS } from './components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepConstants'; import { V2V_VM_STEP_RESET } from './components/PlanWizardVMStep/PlanWizardVMStepConstants'; export const hidePlanWizardAction = () => dispatch => { @@ -25,7 +25,11 @@ export const planWizardExitedAction = () => dispatch => { dispatch({ type: V2V_VM_STEP_RESET }); - dispatch(reset('planWizardOptionsStep')); + dispatch(reset('planWizardScheduleStep')); + dispatch(reset('planWizardAdvancedOptionsStep')); + dispatch({ + type: RESET_V2V_ADVANCED_OPTIONS_STEP_VMS + }); }; export const setPlansBodyAction = body => dispatch => { @@ -61,3 +65,10 @@ export const hideAlertAction = () => dispatch => { type: V2V_PLAN_WIZARD_HIDE_ALERT }); }; + +export const resetAdvancedOptionsStepAction = () => dispatch => { + dispatch({ + type: RESET_V2V_ADVANCED_OPTIONS_STEP_VMS + }); + dispatch(reset('planWizardAdvancedOptionsStep')); +}; diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizardBody.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizardBody.js index 8ff7c6fee4..ff77f80296 100644 --- a/app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizardBody.js +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizardBody.js @@ -4,7 +4,7 @@ import { noop } from 'patternfly-react'; import ModalWizard from '../../components/ModalWizard'; import componentRegistry from '../../../../../../components/componentRegistry'; import PlanWizardGeneralStep from '../PlanWizard/components/PlanWizardGeneralStep'; -import PlanWizardOptionsStep from '../PlanWizard/components/PlanWizardOptionsStep'; +import PlanWizardScheduleStep from '../PlanWizard/components/PlanWizardScheduleStep'; class PlanWizardBody extends React.Component { constructor(props) { @@ -12,6 +12,7 @@ class PlanWizardBody extends React.Component { this.planWizardVMStepContainer = componentRegistry.markup('PlanWizardVMStepContainer'); this.planWizardResultsStepContainer = componentRegistry.markup('PlanWizardResultsStepContainer'); + this.planWizardAdvancedOptionsStepContainer = componentRegistry.markup('PlanWizardAdvancedOptionsStepContainer'); } shouldComponentUpdate(nextProps, nextState) { return JSON.stringify(this.props) !== JSON.stringify(nextProps); @@ -39,8 +40,13 @@ class PlanWizardBody extends React.Component { disableGoto: !this.props.planWizardVMStep.values }, { - title: __('Options'), - render: () => , + title: __('Advanced Options'), + render: () => this.planWizardAdvancedOptionsStepContainer, + disableGoto: true + }, + { + title: __('Schedule'), + render: () => , disableGoto: true }, { diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizardSelectors.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizardSelectors.js index 6b2d2bfbab..e0d014073d 100644 --- a/app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizardSelectors.js +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/PlanWizardSelectors.js @@ -5,5 +5,6 @@ export const planWizardOverviewFilter = overview => ({ export const planWizardFormFilter = form => ({ planWizardGeneralStep: form.planWizardGeneralStep, planWizardVMStep: form.planWizardVMStep, - planWizardOptionsStep: form.planWizardOptionsStep + planWizardAdvancedOptionsStep: form.planWizardAdvancedOptionsStep, + planWizardScheduleStep: form.planWizardScheduleStep }); diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/__tests__/__snapshots__/index.test.js.snap b/app/javascript/react/screens/App/Overview/screens/PlanWizard/__tests__/__snapshots__/index.test.js.snap index 8291422766..ae2059639c 100644 --- a/app/javascript/react/screens/App/Overview/screens/PlanWizard/__tests__/__snapshots__/index.test.js.snap +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/__tests__/__snapshots__/index.test.js.snap @@ -5,16 +5,20 @@ Object { "alertText": undefined, "alertType": undefined, "hideAlertAction": [Function], + "hideConfirmModalAction": [Function], "hidePlanWizard": false, "hidePlanWizardAction": [Function], + "planWizardAdvancedOptionsStep": Object {}, "planWizardExitedAction": [Function], "planWizardGeneralStep": Object {}, - "planWizardOptionsStep": Object {}, + "planWizardScheduleStep": Object {}, "planWizardVMStep": Object {}, + "resetAdvancedOptionsStepAction": [Function], "resetVmStepAction": [Function], "setMigrationsFilterAction": [Function], "setPlanScheduleAction": [Function], "setPlansBodyAction": [Function], "showAlertAction": [Function], + "showConfirmModalAction": [Function], } `; diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStep.fixtures.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStep.fixtures.js new file mode 100644 index 0000000000..00b3ad703e --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStep.fixtures.js @@ -0,0 +1,100 @@ +// ServiceTemplateAnsiblePlaybook + +export const playbooks = [ + { + href: 'http://localhost:3000/api/service_templates/43', + id: '43', + name: 'Ansible test', + description: '', + guid: '7de52447-2946-409d-8e76-b64d0f17803d', + type: 'ServiceTemplateAnsiblePlaybook', + service_template_id: null, + options: { + config_info: { + provision: { + repository_id: '23', + playbook_id: '309', + credential_id: '10', + hosts: 'test_avaleror', + verbosity: '1', + log_output: 'on_error', + extra_vars: {}, + execution_ttl: '5', + become_enabled: false, + cloud_credential_id: '124', + new_dialog_name: 'demo_httpd', + fqname: '/Service/Generic/StateMachines/GenericLifecycle/provision', + dialog_id: '50' + }, + retirement: { + remove_resources: 'no_with_playbook', + verbosity: '0', + log_output: 'on_error', + repository_id: '23', + playbook_id: '305', + credential_id: '10', + execution_ttl: '', + hosts: 'localhost', + extra_vars: {}, + become_enabled: false, + fqname: '/Service/Generic/StateMachines/GenericLifecycle/Retire_Advanced_Resource_None' + } + } + }, + created_at: '2018-02-09T19:33:42Z', + updated_at: '2018-02-11T12:31:20Z', + display: true, + evm_owner_id: null, + miq_group_id: '39', + service_type: 'atomic', + prov_type: 'generic_ansible_playbook', + provision_cost: null, + service_template_catalog_id: '16', + long_description: null, + tenant_id: '1', + generic_subtype: null, + deleted_on: null, + actions: [ + { + name: 'edit', + method: 'post', + href: 'http://localhost:3000/api/service_templates/43' + }, + { + name: 'edit', + method: 'patch', + href: 'http://localhost:3000/api/service_templates/43' + }, + { + name: 'edit', + method: 'put', + href: 'http://localhost:3000/api/service_templates/43' + }, + { + name: 'delete', + method: 'post', + href: 'http://localhost:3000/api/service_templates/43' + }, + { + name: 'order', + method: 'post', + href: 'http://localhost:3000/api/service_templates/43' + }, + { + name: 'archive', + method: 'post', + href: 'http://localhost:3000/api/service_templates/43' + }, + { + name: 'unarchive', + method: 'post', + href: 'http://localhost:3000/api/service_templates/43' + }, + { + name: 'delete', + method: 'delete', + href: 'http://localhost:3000/api/service_templates/43' + } + ] + } +]; diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStep.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStep.js new file mode 100644 index 0000000000..f7033e749b --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStep.js @@ -0,0 +1,107 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Field, reduxForm } from 'redux-form'; +import { Form, Spinner } from 'patternfly-react'; + +import PlanWizardAdvancedOptionsStepTable from './components/PlanWizardAdvancedOptionsStepTable/PlanWizardAdvancedOptionsStepTable'; +import { BootstrapSelect } from '../../../../../common/forms/BootstrapSelect'; + +class PlanWizardAdvancedOptionsStep extends Component { + constructor(props) { + super(props); + + if (props.vms.length === 0) { + props.setVmsAction(props.vmStepSelectedVms); + } + } + + componentDidMount() { + const { fetchPlaybooksAction, fetchPlaybooksUrl } = this.props; + fetchPlaybooksAction(fetchPlaybooksUrl); + } + + onSelectChange = (event, scheduleType) => { + if (event.target.value === '') { + const { change, setVmsAction, vms } = this.props; + change(`playbookVms.${scheduleType}`, []); + setVmsAction(vms.map(vm => ({ ...vm, [scheduleType]: false }))); + } + }; + + render() { + const { playbooks, isFetchingPlaybooks, advancedOptionsStepForm, vms, setVmsAction } = this.props; + + return ( + +
+ this.onSelectChange(event, 'preMigration')} + /> + this.onSelectChange(event, 'postMigration')} + /> + + {advancedOptionsStepForm && + advancedOptionsStepForm.values && ( + + )} +
+ ); + } +} + +PlanWizardAdvancedOptionsStep.propTypes = { + playbooks: PropTypes.array, + isFetchingPlaybooks: PropTypes.bool, + fetchPlaybooksAction: PropTypes.func, + fetchPlaybooksUrl: PropTypes.string, + advancedOptionsStepForm: PropTypes.object, + vms: PropTypes.array, + setVmsAction: PropTypes.func, + vmStepSelectedVms: PropTypes.array, + change: PropTypes.func +}; + +export default reduxForm({ + form: 'planWizardAdvancedOptionsStep', + initialValues: { + playbookVms: { + preMigration: [], + postMigration: [] + }, + preMigrationPlaybook: '', + postMigrationPlaybook: '' + }, + destroyOnUnmount: false +})(PlanWizardAdvancedOptionsStep); diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStep.scss b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStep.scss new file mode 100644 index 0000000000..724a99c72d --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStep.scss @@ -0,0 +1,28 @@ +.table-view-pf-select.with-label { + width: 110px; +} + +.table-view-pf-select-content { + display: flex; + flex-direction: column; + align-items: center; +} + +.table-view-pf-footer { + background: $color-pf-black-150; + &-count { + font-weight: 600; + white-space: pre; + } +} + +.bootstrap-select.btn-group.form-control.preMigrationPlaybook_select, +.bootstrap-select.btn-group.form-control.postMigrationPlaybook_select { + margin-bottom: 20px; +} + +.playbook-services-toolbar .toolbar-pf { + background: $color-pf-black-150; + border: 1px solid #d1d1d1; + border-bottom: none; +} diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepActions.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepActions.js new file mode 100644 index 0000000000..c69c309fcb --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepActions.js @@ -0,0 +1,28 @@ +import URI from 'urijs'; +import API from '../../../../../../../../common/API'; + +import { + FETCH_V2V_PLAYBOOKS, + SET_V2V_ADVANCED_OPTIONS_STEP_VMS, + RESET_V2V_ADVANCED_OPTIONS_STEP_VMS +} from './PlanWizardAdvancedOptionsStepConstants'; + +export const _getPlaybooksActionCreator = url => dispatch => + dispatch({ + type: FETCH_V2V_PLAYBOOKS, + payload: API.get(url) + }); + +export const fetchPlaybooksAction = url => { + const uri = new URI(url); + return _getPlaybooksActionCreator(uri.toString()); +}; + +export const setVmsAction = vms => ({ + type: SET_V2V_ADVANCED_OPTIONS_STEP_VMS, + payload: vms +}); + +export const resetVmsAction = () => ({ + type: RESET_V2V_ADVANCED_OPTIONS_STEP_VMS +}); diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepConstants.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepConstants.js new file mode 100644 index 0000000000..6dab721610 --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepConstants.js @@ -0,0 +1,18 @@ +export const FETCH_V2V_PLAYBOOKS = 'FETCH_V2V_PLAYBOOKS'; +export const SET_V2V_ADVANCED_OPTIONS_STEP_VMS = 'SET_V2V_ADVANCED_OPTIONS_STEP_VMS'; +export const RESET_V2V_ADVANCED_OPTIONS_STEP_VMS = 'RESET_V2V_ADVANCED_OPTIONS_STEP_VMS'; + +export const FILTER_TYPES = [ + { + id: 'name', + title: __('VM Name'), + placeholder: __('Filter by VM Name'), + filterType: 'text' + }, + { + id: 'cluster', + title: __('Source Cluster'), + placeholder: __('Filter by Source Cluster'), + filterType: 'text' + } +]; diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepReducer.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepReducer.js new file mode 100644 index 0000000000..d02504b7f8 --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepReducer.js @@ -0,0 +1,42 @@ +import Immutable from 'seamless-immutable'; + +import { validatePlaybooks } from './PlanWizardAdvancedOptionsStepValidators'; + +import { + FETCH_V2V_PLAYBOOKS, + SET_V2V_ADVANCED_OPTIONS_STEP_VMS, + RESET_V2V_ADVANCED_OPTIONS_STEP_VMS +} from './PlanWizardAdvancedOptionsStepConstants'; + +const initialState = Immutable({ + playbooks: [], + isFetchingPlaybooks: false, + isRejectedPlaybooks: false, + errorPlaybooks: null, + vms: [] +}); + +export default (state = initialState, action) => { + switch (action.type) { + case `${FETCH_V2V_PLAYBOOKS}_PENDING`: + return state.set('isFetchingPlaybooks', true).set('isRejectedPlaybooks', false); + case `${FETCH_V2V_PLAYBOOKS}_FULFILLED`: + validatePlaybooks(action.payload.data.resources); + return state + .set('playbooks', action.payload.data.resources) + .set('isFetchingPlaybooks', false) + .set('isRejectedPlaybooks', false) + .set('errorPlaybooks', null); + case `${FETCH_V2V_PLAYBOOKS}_REJECTED`: + return state + .set('errorPlaybooks', action.payload) + .set('isFetchingPlaybooks', false) + .set('isRejectedPlaybooks', true); + case SET_V2V_ADVANCED_OPTIONS_STEP_VMS: + return state.set('vms', action.payload); + case RESET_V2V_ADVANCED_OPTIONS_STEP_VMS: + return state.set('vms', []); + default: + return state; + } +}; diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepSelectors.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepSelectors.js new file mode 100644 index 0000000000..25f6d21251 --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepSelectors.js @@ -0,0 +1 @@ +export const getVMStepSelectedVms = (allVms, selectedVms) => allVms.filter(vm => selectedVms.includes(vm.id)); diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepValidators.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepValidators.js new file mode 100644 index 0000000000..4ed1827659 --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/PlanWizardAdvancedOptionsStepValidators.js @@ -0,0 +1,6 @@ +import { validateSchema } from '../../../../../../../../data/schemaHelpers'; +import { playbooksSchema } from '../../../../../../../../data/models/serviceTemplateAnsiblePlaybooks'; + +export const validatePlaybooks = playbooks => { + validateSchema(playbooksSchema, playbooks); +}; diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/components/PlanWizardAdvancedOptionsStepTable/PlanWizardAdvancedOptionsStepTable.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/components/PlanWizardAdvancedOptionsStepTable/PlanWizardAdvancedOptionsStepTable.js new file mode 100644 index 0000000000..83632ab561 --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/components/PlanWizardAdvancedOptionsStepTable/PlanWizardAdvancedOptionsStepTable.js @@ -0,0 +1,518 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import orderBy from 'lodash.orderby'; +import * as sort from 'sortabular'; +import * as resolve from 'table-resolver'; +import { compose } from 'recompose'; +import { paginate, Grid, PaginationRow, Table, Toolbar, FormControl, Filter, PAGINATION_VIEW } from 'patternfly-react'; + +import rowFilter from '../../../common/rowFilter'; +import searchFilter from '../../../common/searchFilter'; +import CustomToolbarFind from '../../../common/CustomToolbarFind'; +import vmSelectionHeaderCellFormatter from './vmSelectionHeaderCellFormatter'; +import vmSelectionCellFormatter from './vmSelectionCellFormatter'; +import { FILTER_TYPES } from '../../PlanWizardAdvancedOptionsStepConstants'; + +class PlanWizardAdvancedOptionsStepTable extends React.Component { + static deselectRow(row, scheduleType) { + return { ...row, [scheduleType]: false }; + } + static selectRow(row, scheduleType) { + return { ...row, [scheduleType]: true }; + } + + constructor(props) { + super(props); + + // enables our custom header formatters extensions to reactabular + this.customHeaderFormatters = Table.customHeaderFormattersDefinition; + + this.state = { + // Toolbar Filter state + filterTypes: FILTER_TYPES, + currentFilterType: FILTER_TYPES[0], + currentValue: '', + activeFilters: [], + searchFilterValue: '', + + // Sort the first column in an ascending way by default. + sortingColumns: { + name: { + direction: Table.TABLE_SORT_DIRECTION.ASC, + position: 0 + } + }, + + // pagination default states + pagination: { + page: 1, + perPage: 5, + perPageOptions: [5, 10, 15] + }, + + // page input value + pageChangeValue: 1 + }; + } + onFirstPage = () => { + this.setPage(1); + }; + onLastPage = () => { + const { page } = this.state.pagination; + const totalPages = this.totalPages(); + if (page < totalPages) { + this.setPage(totalPages); + } + }; + onNextPage = () => { + const { page } = this.state.pagination; + if (page < this.totalPages()) { + this.setPage(this.state.pagination.page + 1); + } + }; + onPageInput = e => { + this.setState({ pageChangeValue: e.target.value }); + }; + onPerPageSelect = (eventKey, e) => { + const newPaginationState = Object.assign({}, this.state.pagination); + newPaginationState.perPage = eventKey; + newPaginationState.page = 1; + this.setState({ pagination: newPaginationState }); + }; + onPreviousPage = () => { + if (this.state.pagination.page > 1) { + this.setPage(this.state.pagination.page - 1); + } + }; + + onSelectAllRows = (event, scheduleType) => { + const { input, rows, setVmsAction } = this.props; + const { checked } = event.target; + + const filteredRows = this.filteredSearchedRows(); + const currentRows = this.currentRows(filteredRows).rows; + + if (checked) { + const updatedSelections = [...new Set([...currentRows.map(row => row.id), ...this.props[scheduleType]])]; + + const updatedRows = rows.map( + row => + updatedSelections.indexOf(row.id) > -1 ? PlanWizardAdvancedOptionsStepTable.selectRow(row, scheduleType) : row + ); + + setVmsAction(updatedRows); + input.onChange({ ...input.value, [scheduleType]: updatedSelections }); + } else { + const updatedSelections = this.props[scheduleType].filter(id => !currentRows.some(row => row.id === id)); + + const updatedRows = rows.map( + row => + updatedSelections.indexOf(row.id) > -1 + ? row + : PlanWizardAdvancedOptionsStepTable.deselectRow(row, scheduleType) + ); + + setVmsAction(updatedRows); + input.onChange({ ...input.value, [scheduleType]: updatedSelections }); + } + }; + + onSelectRow = (event, row, scheduleType) => { + const { input, rows, setVmsAction } = this.props; + + const updatedRows = rows.map(r => { + if (r.id === row.id) { + return event.target.checked + ? PlanWizardAdvancedOptionsStepTable.selectRow(r, scheduleType) + : PlanWizardAdvancedOptionsStepTable.deselectRow(r, scheduleType); + } + return r; + }); + + const updatedSelections = event.target.checked + ? [...this.props[scheduleType], row.id] + : this.props[scheduleType].filter(selectedRowId => selectedRowId !== row.id); + + setVmsAction(updatedRows); + input.onChange({ ...input.value, [scheduleType]: updatedSelections }); + }; + + onSubmit = () => { + this.setPage(this.state.pageChangeValue); + }; + onValueKeyPress = keyEvent => { + const { currentValue, currentFilterType } = this.state; + + if (keyEvent.key === 'Enter' && currentValue && currentValue.length > 0) { + this.setState({ currentValue: '' }); + this.filterAdded(currentFilterType, currentValue); + keyEvent.stopPropagation(); + keyEvent.preventDefault(); + } + }; + onFindAction = value => { + // clear filters and set search text (search and filter are independent for now) + this.setState({ activeFilters: [], searchFilterValue: value }); + }; + onFindExit = () => { + this.setState({ searchFilterValue: '' }); + }; + setPage = value => { + const page = Number(value); + if (!Number.isNaN(value) && value !== '' && page > 0 && page <= this.totalPages()) { + const newPaginationState = Object.assign({}, this.state.pagination); + newPaginationState.page = page; + this.setState({ pagination: newPaginationState, pageChangeValue: page }); + } + }; + + getColumns = () => { + const getSortingColumns = () => this.state.sortingColumns || {}; + + const sortableTransform = sort.sort({ + getSortingColumns, + onSort: selectedColumn => { + this.setState({ + sortingColumns: sort.byColumn({ + sortingColumns: this.state.sortingColumns, + sortingOrder: Table.defaultSortingOrder, + selectedColumn + }) + }); + }, + // Use property or index dependening on the sortingColumns structure specified + strategy: sort.strategies.byProperty + }); + + const sortingFormatter = sort.header({ + sortableTransform, + getSortingColumns, + strategy: sort.strategies.byProperty + }); + + return [ + { + property: 'preMigration', + header: { + label: __('Pre-Migration Service'), + props: { + index: 0, + rowSpan: 1, + colSpan: 1, + id: 'pre_migration_select_all', + playbook: { preMigration: this.props.preMigrationPlaybook } + }, + customFormatters: [vmSelectionHeaderCellFormatter] + }, + cell: { + props: { + index: 0 + }, + formatters: [ + (value, { rowData, rowIndex }) => + vmSelectionCellFormatter( + { rowData, rowIndex }, + this.onSelectRow, + `pre_migration_select_${rowIndex}`, + sprintf(__('Pre Migration Select %s'), rowIndex), + { preMigration: this.props.preMigrationPlaybook } + ) + ] + } + }, + { + property: 'postMigration', + header: { + label: __('Post-Migration Service'), + props: { + index: 1, + rowSpan: 1, + colSpan: 1, + id: 'post_migration_select_all', + playbook: { postMigration: this.props.postMigrationPlaybook } + }, + customFormatters: [vmSelectionHeaderCellFormatter] + }, + cell: { + props: { + index: 1 + }, + formatters: [ + (value, { rowData, rowIndex }) => + vmSelectionCellFormatter( + { rowData, rowIndex }, + this.onSelectRow, + `post_migration_select_${rowIndex}`, + sprintf(__('Post Migration Select %s'), rowIndex), + { postMigration: this.props.postMigrationPlaybook } + ) + ] + } + }, + { + property: 'name', + header: { + label: __('VM Name'), + props: { + index: 2, + rowSpan: 1, + colSpan: 1 + }, + transforms: [sortableTransform], + formatters: [sortingFormatter], + customFormatters: [Table.sortableHeaderCellFormatter] + }, + cell: { + props: { + index: 2 + }, + formatters: [Table.tableCellFormatter] + } + }, + { + property: 'cluster', + header: { + label: __('Source Cluster'), + props: { + index: 3, + rowSpan: 1, + colSpan: 1 + }, + transforms: [sortableTransform], + formatters: [sortingFormatter], + customFormatters: [Table.sortableHeaderCellFormatter] + }, + cell: { + props: { + index: 3 + }, + formatters: [Table.tableCellFormatter] + } + } + ]; + }; + + filteredSearchedRows = () => { + const { activeFilters, searchFilterValue } = this.state; + const { rows } = this.props; + if (activeFilters && activeFilters.length) { + return rowFilter(activeFilters, rows); + } else if (searchFilterValue) { + return searchFilter(searchFilterValue, rows); + } + return rows; + }; + + clearFilters = () => { + this.setState({ activeFilters: [] }); + }; + + removeFilter = filter => { + const { activeFilters } = this.state; + + const index = activeFilters.indexOf(filter); + if (index > -1) { + const updated = [...activeFilters.slice(0, index), ...activeFilters.slice(index + 1)]; + this.setState({ activeFilters: updated }); + } + }; + + updateCurrentValue = event => { + this.setState({ currentValue: event.target.value }); + }; + + filterAdded = (field, value) => { + const filterText = `${field.title}: ${value}`; + const activeFilters = [...this.state.activeFilters, { label: filterText, field, value }]; + + this.setState({ activeFilters }); + }; + + selectFilterType = filterType => { + const { currentFilterType } = this.state; + if (currentFilterType !== filterType) { + this.setState({ currentValue: '', currentFilterType: filterType }); + } + }; + + totalPages = () => { + const { rows } = this.props; + const { perPage } = this.state.pagination; + return Math.ceil(rows.length / perPage); + }; + + currentRows = filteredRows => { + const { sortingColumns, pagination } = this.state; + + return compose( + paginate(pagination), + sort.sorter({ + columns: this.getColumns(), + sortingColumns, + sort: orderBy, + strategy: sort.strategies.byProperty + }) + )(filteredRows); + }; + + render() { + const { + pagination, + sortingColumns, + pageChangeValue, + activeFilters, + filterTypes, + currentFilterType, + currentValue + } = this.state; + + const { + rows, + meta: { pristine, error }, + preMigration, + postMigration + } = this.props; + + const filteredRows = this.filteredSearchedRows(); + const sortedPaginatedRows = this.currentRows(filteredRows); + + const tableFooter = ( + + + + {sprintf(__('%s of %s selected'), preMigration.length, rows.length)} + + + {sprintf(__('%s of %s selected'), postMigration.length, rows.length)} + + + + + + ); + + return ( + +

{__('Select VMs on which to run the playbook services')}

+ + + + this.updateCurrentValue(e)} + onKeyPress={e => this.onValueKeyPress(e)} + /> + + + + + {activeFilters && + activeFilters.length > 0 && ( + +
+ {filteredRows.length} {filteredRows.length === 1 ? __('Result') : __('Results')} +
+ {__('Active Filters')}: + + {activeFilters.map((item, index) => ( + + {item.label} + + ))} + + { + e.preventDefault(); + this.clearFilters(); + }} + > + {__('Clear All Filters')} + +
+ )} +
+ + this.customHeaderFormatters({ + cellProps, + columns: this.getColumns(), + sortingColumns, + rows: sortedPaginatedRows.rows, + onSelectAllRows: this.onSelectAllRows + }) + } + }} + > + + + {tableFooter} + + +
+ {pristine && error} +
+ ); + } +} + +PlanWizardAdvancedOptionsStepTable.propTypes = { + rows: PropTypes.array, + input: PropTypes.shape({ + value: PropTypes.shape({ + preMigration: PropTypes.arrayOf(PropTypes.string), + postMigration: PropTypes.arrayOf(PropTypes.string) + }), + onChange: PropTypes.func + }), + meta: PropTypes.shape({ + pristine: PropTypes.bool, + error: PropTypes.string + }), + preMigrationPlaybook: PropTypes.string, + postMigrationPlaybook: PropTypes.string, + setVmsAction: PropTypes.func, + preMigration: PropTypes.array, + postMigration: PropTypes.array +}; + +PlanWizardAdvancedOptionsStepTable.defaultProps = { + rows: [], + preMigration: [], + postMigration: [] +}; + +export default PlanWizardAdvancedOptionsStepTable; diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/components/PlanWizardAdvancedOptionsStepTable/vmSelectionCellFormatter.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/components/PlanWizardAdvancedOptionsStepTable/vmSelectionCellFormatter.js new file mode 100644 index 0000000000..b1b002c464 --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/components/PlanWizardAdvancedOptionsStepTable/vmSelectionCellFormatter.js @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { noop, Table } from 'patternfly-react'; + +const vmSelectionCellFormatter = ({ rowData, rowIndex }, onSelectRow, id, label, playbook) => { + const checkboxId = id || `select${rowIndex}`; + const checkboxLabel = label || sprintf(__('Select row %s'), rowIndex.toString()); + const scheduleType = label.match('Pre') ? 'preMigration' : 'postMigration'; + + return ( + +
+ { + onSelectRow(e, rowData, scheduleType); + }} + disabled={!playbook[scheduleType]} + /> +
+
+ ); +}; +vmSelectionCellFormatter.propTypes = { + /** rowData for this row */ + rowData: PropTypes.object, + /** rowIndex for this row */ + rowIndex: PropTypes.number.isRequired, + /** row selected callback */ + onSelectRow: PropTypes.func, // eslint-disable-line react/no-unused-prop-types + /** checkbox id override */ + id: PropTypes.string, // eslint-disable-line react/no-unused-prop-types + /** checkbox label override */ + label: PropTypes.string // eslint-disable-line react/no-unused-prop-types +}; +vmSelectionCellFormatter.defaultProps = { + rowData: {}, + onSelectRow: noop, + id: '', + label: '' +}; +export default vmSelectionCellFormatter; diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/components/PlanWizardAdvancedOptionsStepTable/vmSelectionHeaderCellFormatter.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/components/PlanWizardAdvancedOptionsStepTable/vmSelectionHeaderCellFormatter.js new file mode 100644 index 0000000000..90970ffe72 --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/components/PlanWizardAdvancedOptionsStepTable/vmSelectionHeaderCellFormatter.js @@ -0,0 +1,48 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { noop, Table } from 'patternfly-react'; + +/** + * Extends PatternFly React selection header cell formatter + * b/c if the CSV input has conflicting/invalid rows, we should disable select all + */ +const vmSelectionHeaderCellFormatter = ({ cellProps, column, rows, onSelectAllRows }) => { + const id = cellProps.id || 'selectAll'; + const { + property: scheduleType, + header: { label } + } = column; + const unselectedRows = rows.filter(r => !r[scheduleType]).length > 0; + const { playbook, ...otherCellProps } = cellProps; + return ( + +
+
{label}
+ onSelectAllRows(event, scheduleType)} + disabled={!playbook[scheduleType]} + /> +
+
+ ); +}; +vmSelectionHeaderCellFormatter.propTypes = { + /** column header cell props */ + cellProps: PropTypes.object, + /** column definition */ + column: PropTypes.object, + /** current table rows */ + rows: PropTypes.array, + /** on select all rows callback */ + onSelectAllRows: PropTypes.func +}; +vmSelectionHeaderCellFormatter.defaultProps = { + cellProps: {}, + column: {}, + rows: [], + onSelectAllRows: noop +}; +export default vmSelectionHeaderCellFormatter; diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/index.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/index.js new file mode 100644 index 0000000000..5e2b850294 --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardAdvancedOptionsStep/index.js @@ -0,0 +1,49 @@ +import { connect } from 'react-redux'; + +import PlanWizardAdvancedOptionsStep from './PlanWizardAdvancedOptionsStep'; +import * as PlanWizardAdvancedOptionsStepActions from './PlanWizardAdvancedOptionsStepActions'; +import { getVMStepSelectedVms } from './PlanWizardAdvancedOptionsStepSelectors'; +import reducer from './PlanWizardAdvancedOptionsStepReducer'; + +export const reducers = { planWizardAdvancedOptionsStep: reducer }; + +const mapStateToProps = ( + { + planWizardAdvancedOptionsStep, + planWizardVMStep, + form: { + planWizardGeneralStep: { + values: { vm_choice_radio } + }, + planWizardVMStep: { + values: { selectedVms } + }, + planWizardAdvancedOptionsStep: advancedOptionsStepForm + } + }, + ownProps +) => { + const allVms = + vm_choice_radio === 'vms_via_csv' + ? [...planWizardVMStep.valid_vms, ...planWizardVMStep.invalid_vms, ...planWizardVMStep.conflict_vms] + : planWizardVMStep.valid_vms; + + return { + ...planWizardAdvancedOptionsStep, + ...ownProps.data, + advancedOptionsStepForm, + vmStepSelectedVms: getVMStepSelectedVms(allVms, selectedVms) + }; +}; + +const mergeProps = (stateProps, dispatchProps, ownProps) => ({ + ...stateProps, + ...ownProps.data, + ...dispatchProps +}); + +export default connect( + mapStateToProps, + PlanWizardAdvancedOptionsStepActions, + mergeProps +)(PlanWizardAdvancedOptionsStep); diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardGeneralStep/PlanWizardGeneralStep.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardGeneralStep/PlanWizardGeneralStep.js index 187c3fe9ff..13e42b9073 100644 --- a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardGeneralStep/PlanWizardGeneralStep.js +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardGeneralStep/PlanWizardGeneralStep.js @@ -21,6 +21,9 @@ const PlanWizardGeneralStep = ({ transformationMappings }) => ( option_key="id" option_value="name" form_name="planWizardGeneralStep" + inline_label + labelWidth={2} + controlWidth={9} /> ( +const PlanWizardScheduleStep = () => (
( ); export default reduxForm({ - form: 'planWizardOptionsStep', + form: 'planWizardScheduleStep', destroyOnUnmount: false -})(PlanWizardOptionsStep); +})(PlanWizardScheduleStep); diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardOptionsStep/index.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardScheduleStep/index.js similarity index 55% rename from app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardOptionsStep/index.js rename to app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardScheduleStep/index.js index 01bb20c9dc..7aa9efd0fa 100644 --- a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardOptionsStep/index.js +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardScheduleStep/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import PlanWizardOptionsStep from './PlanWizardOptionsStep'; +import PlanWizardScheduleStep from './PlanWizardScheduleStep'; const mapStateToProps = () => ({ initialValues: { @@ -7,4 +7,4 @@ const mapStateToProps = () => ({ } }); -export default connect(mapStateToProps)(PlanWizardOptionsStep); +export default connect(mapStateToProps)(PlanWizardScheduleStep); diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardVMStep/components/PlanWizardVMStepTable.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardVMStep/components/PlanWizardVMStepTable.js index e566f72a67..fab3333586 100644 --- a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardVMStep/components/PlanWizardVMStepTable.js +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/PlanWizardVMStep/components/PlanWizardVMStepTable.js @@ -21,9 +21,9 @@ import { PAGINATION_VIEW } from 'patternfly-react'; -import rowFilter from './rowFilter'; -import searchFilter from './searchFilter'; -import CustomToolbarFind from './CustomToolbarFind'; +import rowFilter from '../../common/rowFilter'; +import searchFilter from '../../common/searchFilter'; +import CustomToolbarFind from '../../common/CustomToolbarFind'; import vmSelectionHeaderCellFormatter from './vmSelectionHeaderCellFormatter'; import vmSelectionCellFormatter from './vmSelectionCellFormatter'; diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/common/CustomToolbarFind.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/common/CustomToolbarFind.js new file mode 100644 index 0000000000..b09625d007 --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/common/CustomToolbarFind.js @@ -0,0 +1,104 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { noop, Button, Icon, FormControl } from 'patternfly-react'; + +/** + * This is a custom toolbar find w/ limited functionality of Toolbar.Find + * Note: does not implement the "findNext" and "findPrevious" functionality + * + * This should be added in a future release. + */ +class CustomToolbarFind extends React.Component { + state = { + dropdownShown: false, + currentValue: '' + }; + + onValueKeyPress = keyEvent => { + const { onEnter } = this.props; + const { currentValue } = this.state; + + if (keyEvent.key === 'Enter' && onEnter) { + onEnter(currentValue); + } + }; + + handleValueChange = event => { + const { onChange } = this.props; + + this.setState({ currentValue: event.target.value }); + + if (onChange) { + onChange(event.target.value); + } + }; + + hideDropdown = () => { + const { onExit } = this.props; + this.setState({ dropdownShown: false }); + onExit(); + }; + + toggleDropdownShown = () => { + this.setState(prevState => ({ dropdownShown: !prevState.dropdownShown })); + }; + + render() { + const { dropdownShown, currentValue } = this.state; + const { className, placeholder } = this.props; + + const classes = classNames('form-group toolbar-pf-find', className); + + const dropdownClasses = classNames('find-pf-dropdown-container', { + show: dropdownShown + }); + + return ( +
+ +
+ this.onValueKeyPress(e)} + onChange={this.handleValueChange} + /> +
+ {/* {this.renderCounts()} */} + +
+
+
+ ); + } +} + +CustomToolbarFind.propTypes = { + /** Additional css classes */ + className: PropTypes.string, + /** Placeholder text when empty */ + placeholder: PropTypes.string, + /** Callback function when user hits the enter key */ + onEnter: PropTypes.func, + /** Callback function when the find value changes */ + onChange: PropTypes.func, + /** Callback function when exit clicked */ + onExit: PropTypes.func +}; + +CustomToolbarFind.defaultProps = { + className: '', + placeholder: '', + onEnter: noop, + onChange: noop, + onExit: noop +}; + +export default CustomToolbarFind; diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/common/rowFilter.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/common/rowFilter.js new file mode 100644 index 0000000000..eff7a65a12 --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/common/rowFilter.js @@ -0,0 +1,29 @@ +/** + * Simple client side row filter for VM table + * + * activeFilters: { + * label: 'VM Name: {value}', + * field: {id: "name", title: "VM Name", placeholder: "Filter by VM Name", filterType: "text"}, + * value: '{value}' + * } + * + * rows: [{ + * name: 'cfmetest67', + * path: 'vCenter/Datacenter', + * cluster: 'Raleigh' + * ... + * }] + */ +export default function rowFilter(activeFilters, rows) { + if (activeFilters && activeFilters.length && rows && rows.length) { + const filteredRows = []; + rows.forEach(row => { + const match = activeFilters.every(filter => `${row[filter.field.id] || ''}`.indexOf(filter.value) > -1); + if (match) { + filteredRows.push(row); + } + }); + return filteredRows; + } + return rows; +} diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/common/searchFilter.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/common/searchFilter.js new file mode 100644 index 0000000000..88f078a002 --- /dev/null +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/components/common/searchFilter.js @@ -0,0 +1,23 @@ +/** + * Simple client side search filter for VM table + * + * rows: [{ + * name: 'cfmetest67', + * path: 'vCenter/Datacenter', + * cluster: 'Raleigh' + * ... + * }] + */ +export default function searchFilter(searchFilterValue, rows) { + if (searchFilterValue && rows && rows.length) { + const filteredRows = []; + rows.forEach(row => { + const match = Object.values(row).some(value => `${value || ''}`.indexOf(searchFilterValue) > -1); + if (match) { + filteredRows.push(row); + } + }); + return filteredRows; + } + return rows; +} diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/helpers.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/helpers.js index 2bc4a11df7..44900e1f34 100644 --- a/app/javascript/react/screens/App/Overview/screens/PlanWizard/helpers.js +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/helpers.js @@ -1,17 +1,29 @@ -export const createMigrationPlans = (planWizardGeneralStep, planWizardVMStep) => { +export const createMigrationPlans = (planWizardGeneralStep, planWizardVMStep, planWizardAdvancedOptionsStep) => { const planName = planWizardGeneralStep.values.name; const planDescription = planWizardGeneralStep.values.description; const infrastructureMapping = planWizardGeneralStep.values.infrastructure_mapping; const vms = planWizardVMStep.values.selectedVms; + + const { + playbookVms: { preMigration, postMigration }, + preMigrationPlaybook, + postMigrationPlaybook + } = planWizardAdvancedOptionsStep.values; + const actions = vms.map(vmId => ({ - vm_id: vmId + vm_id: vmId, + pre_service: preMigration.includes(vmId), + post_service: postMigration.includes(vmId) })); + return { name: planName, description: planDescription, prov_type: 'generic_transformation_plan', config_info: { transformation_mapping_id: infrastructureMapping, + pre_service_id: preMigrationPlaybook, + post_service_id: postMigrationPlaybook, actions } }; diff --git a/app/javascript/react/screens/App/Overview/screens/PlanWizard/index.js b/app/javascript/react/screens/App/Overview/screens/PlanWizard/index.js index 6a7d97bf4a..c2c943e106 100644 --- a/app/javascript/react/screens/App/Overview/screens/PlanWizard/index.js +++ b/app/javascript/react/screens/App/Overview/screens/PlanWizard/index.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import PlanWizard from './PlanWizard'; import * as PlanWizardActions from './PlanWizardActions'; import { planWizardOverviewFilter, planWizardFormFilter } from './PlanWizardSelectors'; -import { setMigrationsFilterAction } from '../../OverviewActions'; +import { setMigrationsFilterAction, showConfirmModalAction, hideConfirmModalAction } from '../../OverviewActions'; import reducer from './PlanWizardReducer'; @@ -21,7 +21,9 @@ const mapStateToProps = ({ overview, planWizard, form }, ownProps) => { const actions = { ...PlanWizardActions, - setMigrationsFilterAction + setMigrationsFilterAction, + showConfirmModalAction, + hideConfirmModalAction }; const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(stateProps, ownProps.data, dispatchProps); diff --git a/app/javascript/react/screens/App/common/forms/BootstrapSelect.js b/app/javascript/react/screens/App/common/forms/BootstrapSelect.js index cddc6bbae1..0e665569ea 100644 --- a/app/javascript/react/screens/App/common/forms/BootstrapSelect.js +++ b/app/javascript/react/screens/App/common/forms/BootstrapSelect.js @@ -24,7 +24,7 @@ export class BootstrapSelect extends React.Component { }); } - renderFormGroup = (labelWidth, controlWidth) => { + renderFormGroup = () => { const { input, label, @@ -36,24 +36,30 @@ export class BootstrapSelect extends React.Component { choose_text, meta: { visited, error, active } } = this.props; - const formGroupProps = { key: { label }, ...this.props }; + + const { inline_label, stacked_label, labelWidth, controlWidth, allowClear, ...otherProps } = this.props; + + const formGroupProps = { key: { label }, ...otherProps }; if (visited && !active && error) formGroupProps.validationState = 'error'; return ( - - {label} - {required && ' *'} - + {inline_label && ( + + {label} + {required && ' *'} + + )} + {stacked_label &&

{label}

}