From d560e2842d77e718a0d38b8f4401dd59500b1e9f Mon Sep 17 00:00:00 2001 From: Samuel Liu Date: Fri, 24 May 2024 13:46:57 -0400 Subject: [PATCH] Convert automate simulation form and combine with summary component --- .../application_controller/automate.rb | 61 +- app/controllers/miq_ae_tools_controller.rb | 45 ++ .../button/ae_copy_simulate.rb | 8 - .../toolbar/miq_ae_tools_simulate_center.rb | 10 - app/helpers/miq_ae_tools_helper.rb | 2 +- .../components/AutomationSimulation/index.jsx | 11 +- .../automate-simulation-form.schema.js | 238 +++++++ .../automate-simulation-form/index.jsx | 156 +++++ .../packs/component-definitions-common.js | 2 + .../automate-simulation-form.spec.js.snap | 606 ++++++++++++++++++ .../automate-simulation-form.spec.js | 93 +++ app/stylesheet/application-webpack.scss | 1 + app/stylesheet/automate-simulation.scss | 102 +++ .../layouts/_ae_resolve_options.html.haml | 129 +--- app/views/layouts/_content.html.haml | 65 +- app/views/miq_ae_tools/_resolve.html.haml | 3 - .../miq_ae_tools/_results_tabs.html.haml | 1 - .../miq_ae_tools/resolve_react.html.haml | 1 + app/views/shared/buttons/_ab_form.html.haml | 21 - config/routes.rb | 4 + cypress/downloads/downloads.html | Bin 0 -> 45187 bytes .../e2e/ui/Embedded-Automate/simulation.cy.js | 52 ++ .../buttons/ae_copy_simulate_spec.rb | 17 - .../shared/buttons/_ab_form.html.haml_spec.rb | 30 - 24 files changed, 1367 insertions(+), 291 deletions(-) delete mode 100644 app/helpers/application_helper/button/ae_copy_simulate.rb create mode 100644 app/javascript/components/automate-simulation-form/automate-simulation-form.schema.js create mode 100644 app/javascript/components/automate-simulation-form/index.jsx create mode 100644 app/javascript/spec/automate-simulation-form/__snapshots__/automate-simulation-form.spec.js.snap create mode 100644 app/javascript/spec/automate-simulation-form/automate-simulation-form.spec.js create mode 100644 app/stylesheet/automate-simulation.scss delete mode 100644 app/views/miq_ae_tools/_results_tabs.html.haml create mode 100644 app/views/miq_ae_tools/resolve_react.html.haml create mode 100644 cypress/downloads/downloads.html create mode 100644 cypress/e2e/ui/Embedded-Automate/simulation.cy.js delete mode 100644 spec/helpers/application_helper/buttons/ae_copy_simulate_spec.rb delete mode 100644 spec/views/shared/buttons/_ab_form.html.haml_spec.rb diff --git a/app/controllers/application_controller/automate.rb b/app/controllers/application_controller/automate.rb index e2fbf551043..9637ab5dd57 100644 --- a/app/controllers/application_controller/automate.rb +++ b/app/controllers/application_controller/automate.rb @@ -1,5 +1,6 @@ module ApplicationController::Automate extend ActiveSupport::Concern + include MiqAeToolsHelper def resolve_button_throw if valid_resolve_object? @@ -21,48 +22,10 @@ def resolve_button_throw add_flash(_("Automation Error: %{error_message}") % {:error_message => bang.message}, :error) end end - render :update do |page| - page << javascript_prologue - page.replace("left_cell_bottom", :partial => "resolve_form_buttons") - page.replace("flash_msg_div", :partial => "layouts/flash_msg") - page << "miqScrollTop();" if @flash_array.present? - page.replace_html("main_div", :partial => "results_tabs") - page << javascript_reload_toolbars - page << "miqSparkle(false);" - end + automation_simulation_data(@ae_simulation_tree, @results, @resolve) end private :resolve_button_throw - # Copy current URI as an automate button - def resolve_button_copy - session[:resolve_object] = copy_hash(@resolve) - head :ok - end - private :resolve_button_copy - - # Copy current URI as an automate button - def resolve_button_paste - @resolve = copy_hash(session[:resolve_object]) - @edit = session[:edit] - @custom_button = @edit[:custom_button] - @edit[:instance_names] = @resolve[:instance_names] - @edit[:new][:instance_name] = @resolve[:new][:instance_name] - @edit[:new][:object_message] = @resolve[:new][:object_message] - @edit[:new][:object_request] = @resolve[:new][:object_request] - @edit[:new][:attrs] = @resolve[:new][:attrs] - @edit[:new][:target_class] = @resolve[:target_class] = @resolve[:new][:target_class] - @edit[:uri] = @resolve[:uri] - (ApplicationController::AE_MAX_RESOLUTION_FIELDS - @resolve[:new][:attrs].length).times { @edit[:new][:attrs].push([]) } - @changed = (@edit[:new] != @edit[:current]) - render :update do |page| - page << javascript_prologue - page.replace_html("main_div", :partial => "shared/buttons/ab_list") - page << javascript_for_miq_button_visibility_changed(@changed) - page << "miqSparkle(false);" - end - end - private :resolve_button_paste - # Copy current URI as an automate button def resolve_button_simulate @edit = copy_hash(session[:resolve]) @@ -115,7 +78,7 @@ def resolve_button_reset_or_none end private :resolve_button_reset_or_none - def resolve + def resolve_automate_simulation custom_button_redirect = params[:button] == 'simulate' || params[:simulate] == 'simulate' assert_privileges(custom_button_redirect ? 'ab_button_simulate' : 'miq_ae_class_simulation') @explorer = true @@ -123,7 +86,7 @@ def resolve drop_breadcrumb(:name => _("Resolve"), :url => "/miq_ae_tools/resolve") @lastaction = "resolve" @right_cell_text = _("Simulation") - + get_simulation_form_vars case params[:button] when "throw", "retry" then resolve_button_throw when "copy" then resolve_button_copy @@ -133,6 +96,22 @@ def resolve end end + def resolve + custom_button_redirect = params[:button] == 'simulate' || params[:simulate] == 'simulate' + assert_privileges(custom_button_redirect ? 'ab_button_simulate' : 'miq_ae_class_simulation') + @explorer = true + @breadcrumbs = [] + drop_breadcrumb(:name => _("Resolve"), :url => "/miq_ae_tools/resolve") + @lastaction = "resolve" + @right_cell_text = _("Simulation") + + case params[:button] + when "throw", "retry" then resolve_button_throw + when "simulate" then resolve_button_simulate + else resolve_button_reset_or_none + end + end + def build_results options = { :vmdb_object => @sb[:obj], diff --git a/app/controllers/miq_ae_tools_controller.rb b/app/controllers/miq_ae_tools_controller.rb index 453a77ce504..1163ad542f4 100644 --- a/app/controllers/miq_ae_tools_controller.rb +++ b/app/controllers/miq_ae_tools_controller.rb @@ -329,6 +329,51 @@ def reset_datastore javascript_flash(:spinner_off => true) end + def get_simulation_form_vars + assert_privileges('miq_ae_class_simulation') + if params[:object_request] + @resolve[:new][:object_request] = params[:object_request] + end + if params.key?(:starting_object) + @resolve[:new][:starting_object] = params[:starting_object] + @resolve[:new][:instance_name] = nil + end + if params[:readonly] + @resolve[:new][:readonly] = (params[:readonly] != "1") + end + + copy_params_if_present(@resolve[:new], params, %i[instance_name other_name object_message object_request target_class target_id]) + + ApplicationController::AE_MAX_RESOLUTION_FIELDS.times do |i| + ApplicationController::AE_MAX_RESOLUTION_FIELDS.times do |i| + f = ("attribute_" + (i + 1).to_s) + v = ("value_" + (i + 1).to_s) + @resolve[:new][:attrs][i][0] = params[f.to_sym] || nil + @resolve[:new][:attrs][i][1] = params[v.to_sym] || nil + end + end + @resolve[:new][:target_id] = nil if params[:target_class] == "" + copy_params_if_present(@resolve, params, %i[button_text button_number]) + @resolve[:throw_ready] = ready_to_throw + end + + def get_form_targets + assert_privileges('miq_ae_class_simulation') + if params.key?(:target_class) && params[:target_class] != '-1' + targets = Rbac.filtered(params[:target_class]).select(:id, *columns_for_klass(params[:target_class])) if params[:target_class].present? + unless targets.nil? + @resolve[:targets] = targets.sort_by { |t| t.name.downcase }.collect { |t| [t.name, t.id.to_s] } + if !@resolve[:target_id] + @resolve[:target_id] = nil + end + end + end + + render_json = {} + render_json[:targets] = @resolve[:targets] if @resolve[:targets].present? + render :json => render_json + end + private ########################### def automate_import_json_serializer diff --git a/app/helpers/application_helper/button/ae_copy_simulate.rb b/app/helpers/application_helper/button/ae_copy_simulate.rb deleted file mode 100644 index 5737bb7d8e6..00000000000 --- a/app/helpers/application_helper/button/ae_copy_simulate.rb +++ /dev/null @@ -1,8 +0,0 @@ -class ApplicationHelper::Button::AeCopySimulate < ApplicationHelper::Button::ButtonWithoutRbacCheck - def disabled? - if @resolve[:button_class].blank? - @error_message = _('Object attribute must be specified to copy object details for use in a Button') - @error_message.present? - end - end -end diff --git a/app/helpers/application_helper/toolbar/miq_ae_tools_simulate_center.rb b/app/helpers/application_helper/toolbar/miq_ae_tools_simulate_center.rb index d3c6a8c4a9b..74df5aec9bf 100644 --- a/app/helpers/application_helper/toolbar/miq_ae_tools_simulate_center.rb +++ b/app/helpers/application_helper/toolbar/miq_ae_tools_simulate_center.rb @@ -1,12 +1,2 @@ class ApplicationHelper::Toolbar::MiqAeToolsSimulateCenter < ApplicationHelper::Toolbar::Basic - button_group('miq_ae_tools_vmdb', [ - button( - :ae_copy_simulate, - 'fa fa-files-o fa-lg', - N_('Copy object details for use in a Button'), - N_('Copy'), - :url => "resolve", - :url_parms => "?button=copy", - :klass => ApplicationHelper::Button::AeCopySimulate), - ]) end diff --git a/app/helpers/miq_ae_tools_helper.rb b/app/helpers/miq_ae_tools_helper.rb index 7a3ed3d422b..b86bd60ab85 100644 --- a/app/helpers/miq_ae_tools_helper.rb +++ b/app/helpers/miq_ae_tools_helper.rb @@ -18,7 +18,7 @@ def git_import_submit_help def automation_simulation_data(tree, results, resolve) if results - { + render :json => { :tree => {:text => _('Tree View'), :rows => ae_result_tree(tree)}, :xml => {:text => _('Xml View'), :rows => ae_result_xml(results)}, :object => {:text => _('Object info'), :rows => ae_result_uri(resolve)} diff --git a/app/javascript/components/AutomationSimulation/index.jsx b/app/javascript/components/AutomationSimulation/index.jsx index 2aa68a1ba90..d910d0098c2 100644 --- a/app/javascript/components/AutomationSimulation/index.jsx +++ b/app/javascript/components/AutomationSimulation/index.jsx @@ -7,11 +7,12 @@ import MiqStructuredList from '../miq-structured-list'; /** Component to render the summary contents displayed in the Automation / Embedded Automate / Simulation */ const AutomationSimulation = ({ data }) => { const [tabConfig, setTabConfig] = useState([]); - useEffect(() => { - const config = Object.keys(data).map((name) => ({ name, text: data[name].text })); - setTabConfig(config); - }, []); + if (Object.keys(data).length > 1) { + const config = Object.keys(data).map((name) => ({ name, text: data[name].text })); + setTabConfig(config); + } + }, [data]); /** Function to render the tabs contents. */ const renderTabContent = (name) => { @@ -37,7 +38,7 @@ const AutomationSimulation = ({ data }) => { ); - return data.notice + return Object.keys(data).length <= 1 ? : renderTabs(); }; diff --git a/app/javascript/components/automate-simulation-form/automate-simulation-form.schema.js b/app/javascript/components/automate-simulation-form/automate-simulation-form.schema.js new file mode 100644 index 00000000000..33576984516 --- /dev/null +++ b/app/javascript/components/automate-simulation-form/automate-simulation-form.schema.js @@ -0,0 +1,238 @@ +import { componentTypes, validatorTypes } from '@@ddf'; + +const targetsURL = (targetClass) => `/miq_ae_tools/get_form_targets?target_class=${encodeURIComponent(targetClass)}`; +const loadTargets = (selectedTargetClass) => http.get(targetsURL(selectedTargetClass)) + .then((formVars) => { + if (formVars && formVars.targets) { + return [ + { label: `<${__('None')}>`, value: '-1' }, + ...formVars.targets.map(([key, value]) => ({ + label: String(key), + value: String(value), + })), + ]; + } + return []; + }); + +const createSchema = ( + resolve, maxNameLength, url, attrValuesPairs, maxLength, typeClassesOptions, formData, setFormData, +) => { + const fields = [ + { + component: componentTypes.PLAIN_TEXT, + id: 'object_details', + name: 'object_details', + className: 'automate-object-details', + label: __('Object Details'), + style: { fontSize: '16px' }, + }, + + { + component: componentTypes.SELECT, + id: 'instance_name', + name: 'instance_name', + className: 'automate-instance-name', + label: __('System/Process'), + initialValue: resolve.instance_names.sort((b, a) => a.toLowerCase().localeCompare(b.toLowerCase())), + validate: [{ type: validatorTypes.REQUIRED }], + isSearchable: true, + simpleValue: true, + options: resolve.instance_names.map((name) => ({ label: name, value: name })), + url, + }, + { + component: componentTypes.TEXT_FIELD, + id: 'object_message', + name: 'object_message', + className: 'automate-object-message', + label: __('Message'), + maxLength: maxNameLength, + initialValue: resolve.new.object_message, + isRequired: true, + }, + + { + component: componentTypes.TEXT_FIELD, + id: 'object_request', + name: 'object_request', + className: 'automate-object-request', + label: __('Request'), + initialValue: resolve.new.object_request, + }, + + { + component: componentTypes.PLAIN_TEXT, + id: 'object_attribute', + name: 'object_attribute', + className: 'automate-object-attribute', + label: __('Object Attribute'), + style: { fontSize: '16px' }, + }, + + { + component: componentTypes.SELECT, + id: 'target_class', + name: 'target_class', + label: __('Type'), + options: typeClassesOptions, + initialValue: resolve.new.target_class, + className: 'automate-target-class', + isSearchable: true, + simpleValue: true, + onChange: (targetClass) => { + if (formData.targetClass !== targetClass) { + setFormData((prevData) => ({ ...prevData, targetClass })); + } + }, + validate: [ + { + type: validatorTypes.REQUIRED, + condition: { + not: { + or: [ + { + when: 'target_class', + is: '-1', + }, + { + when: 'target_class', + isEmpty: true, + }, + ], + }, + }, + }, + ], + }, + + { + component: componentTypes.SELECT, + id: 'selection_target', + name: 'selection_target', + label: __('Selection'), + key: `selection_target_${formData.targetClass}`, + className: 'automate-selection-target', + initialValue: resolve.new.target_id, + loadOptions: () => (loadTargets(formData.targetClass)), + condition: { + not: { + or: [ + { + when: 'target_class', + is: '-1', + }, + { + when: 'target_class', + isEmpty: true, + }, + ], + }, + }, + validate: [ + { + type: validatorTypes.REQUIRED, + condition: { + not: { + or: [ + { + when: 'target_class', + is: '-1', + }, + { + when: 'target_class', + isEmpty: true, + }, + ], + }, + }, + }, + ], + }, + { + id: 'simulationParameters', + component: componentTypes.PLAIN_TEXT, + name: 'simulationParameters', + className: 'automate-simulation-parameters', + label: __('Simulation Parameters'), + style: { fontSize: '16px' }, + }, + { + component: componentTypes.CHECKBOX, + id: 'readonly', + name: 'readonly', + className: 'automate-readonly', + label: __('Execute Methods'), + initialValue: resolve.new.readonly, + title: 'Simulation parameters', + }, + { + id: 'AttributeValuePairs', + component: componentTypes.PLAIN_TEXT, + name: 'AttributeValuePairs', + label: __('Attribute/Value Pairs'), + style: { fontSize: '16px' }, + }, + ]; + + if (!document.getElementById('description') && document.getElementById('object_message')) { + document.getElementById('object_message').focus(); + } + + attrValuesPairs.forEach((_, i) => { + const f = `attribute_${i + 1}`; + const v = `value_${i + 1}`; + const labelKey = `attributeValuePairLabel_${i + 1}`; + + const subForm = [ + { + component: componentTypes.SUB_FORM, + id: `subform_${i + 1}`, + name: `subform_${i + 1}`, + className: 'subform', + fields: [ + { + component: componentTypes.PLAIN_TEXT, + id: labelKey, + name: labelKey, + className: 'attributeValuePairLabel', + label: `${i + 1}`, + style: { fontWeight: 'bold' }, + }, + { + component: componentTypes.TEXT_FIELD, + id: f, + name: f, + maxLength, + label: ' ', + initialValue: resolve.new.attrs[i][0], + fieldprops: { + className: 'field-input', + 'data-miq_observe': JSON.stringify({ interval: '.5', url }), + }, + }, + { + component: componentTypes.TEXT_FIELD, + id: v, + name: v, + maxLength, + label: ' ', + initialValue: resolve.new.attrs[i][1], + fieldprops: { + className: 'value-input', + 'data-miq_observe': JSON.stringify({ interval: '.5', url }), + }, + }, + ], + }, + ]; + fields.push(subForm); + }); + + return { + title: 'Object Details', + fields, + }; +}; + +export default createSchema; diff --git a/app/javascript/components/automate-simulation-form/index.jsx b/app/javascript/components/automate-simulation-form/index.jsx new file mode 100644 index 00000000000..9f37581044c --- /dev/null +++ b/app/javascript/components/automate-simulation-form/index.jsx @@ -0,0 +1,156 @@ +import React, { useState, useEffect } from 'react'; +import MiqFormRenderer, { useFormApi } from '@@ddf'; +import { FormSpy } from '@data-driven-forms/react-form-renderer'; +import PropTypes from 'prop-types'; +import { Loading, Button } from 'carbon-components-react'; +import createSchema from './automate-simulation-form.schema'; +import AutomationSimulation from '../AutomationSimulation'; + +const AutomateSimulationForm = ({ + resolve, maxNameLength, url, attrValuesPairs, maxLength, +}) => { + const typeClassesOptions = [ + { label: `<${__('None')}>`, value: '-1' }, + ...Object.entries(resolve.target_classes).map(([key, value]) => ({ label: value, value: key })), + ]; + + const [formData, setFormData] = useState({ + isLoading: false, + tempData: undefined, + targetClass: '-1', + simulationTree: { notice: 'Enter Automation Simulation options on the left and press Submit' }, + }); + + useEffect(() => { + if (formData.isLoading) { + http.post(url, formData.tempData, { + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((result) => { + setFormData({ + ...formData, + isLoading: false, + simulationTree: result, + }); + add_flash(__('Automation simulation has been run'), 'success'); + }) + .catch((error) => console.log('error: ', error)); + } + }, [formData.isLoading]); + + const handleSubmit = (values) => { + const instanceName = Array.isArray(values.instance_name) ? values.instance_name[0] : values.instance_name; + const data = { + instance_name: instanceName, + object_message: values.object_message, + object_request: values.object_request, + target_class: values.target_class, + readonly: values.readonly, + target_id: values.selection_target, + button: 'throw', + }; + + const attributes = Array.from({ length: attrValuesPairs.length }, (_, i) => i + 1).flatMap((i) => [`attribute_${i}`, `value_${i}`]); + const attrValPairs = Object.fromEntries( + attributes.flatMap((key) => (values[key] ? [[key, values[key]]] : [])) + ); + + Object.entries(attrValPairs).forEach(([key, value]) => { + data[key] = value; + }); + + setFormData({ + ...formData, + isLoading: true, + tempData: data, + }); + }; + + const onFormReset = () => { + const buttons = document.querySelectorAll('.bx--list-box__selection'); + buttons.forEach((button) => button.click()); + document.getElementById('object_request').value = ''; + add_flash(__('All changes have been reset'), 'warning'); + }; + + return ( +
+
+ } + /> +
+
+
+ {__('Simulation')} +
+
+ {formData.isLoading ? ( +
+ +
+ ) : ( + + )} +
+
+
+ ); +}; + +const FormTemplate = ({ + formFields, +}) => { + const { + handleSubmit, onReset, getState, + } = useFormApi(); + const { valid } = getState(); + const submitLabel = __('Save'); + return ( +
+ {formFields} + + {() => ( +
+ + +
+ )} +
+
+ ); +}; + +AutomateSimulationForm.propTypes = { + resolve: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired, + maxNameLength: PropTypes.number.isRequired, + url: PropTypes.string.isRequired, + attrValuesPairs: PropTypes.arrayOf(PropTypes.number).isRequired, + maxLength: PropTypes.number.isRequired, +}; + +FormTemplate.propTypes = { + formFields: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired, +}; + +export default AutomateSimulationForm; diff --git a/app/javascript/packs/component-definitions-common.js b/app/javascript/packs/component-definitions-common.js index f56709a0b77..6eea9ae75ce 100644 --- a/app/javascript/packs/component-definitions-common.js +++ b/app/javascript/packs/component-definitions-common.js @@ -19,6 +19,7 @@ import AnsiblePlaybookWorkflow from '../components/ansible-playbook-workflow'; import AnsibleRepositoryForm from '../components/ansible-repository-form'; import AttachDetachCloudVolumeForm from '../components/cloud-volume-form/attach-detach-cloud-volume-form'; import AuthKeypairCloudForm from '../components/auth-key-pair-cloud'; +import AutomateSimulationForm from '../components/automate-simulation-form'; import AutomationSimulation from '../components/AutomationSimulation'; import ButtonList from '../components/data-tables/button-list'; import ButtonGroupList from '../components/data-tables/button-group-list'; @@ -195,6 +196,7 @@ ManageIQ.component.addReact('AnsiblePlaybookWorkflow', AnsiblePlaybookWorkflow); ManageIQ.component.addReact('AnsibleRepositoryForm', AnsibleRepositoryForm); ManageIQ.component.addReact('AttachDetachCloudVolumeForm', AttachDetachCloudVolumeForm); ManageIQ.component.addReact('AuthKeypairCloudForm', AuthKeypairCloudForm); +ManageIQ.component.addReact('AutomateSimulationForm', AutomateSimulationForm); ManageIQ.component.addReact('AutomationSimulation', AutomationSimulation); ManageIQ.component.addReact('BreadcrumbsBar', BreadcrumbsBar); ManageIQ.component.addReact('ButtonList', ButtonList); diff --git a/app/javascript/spec/automate-simulation-form/__snapshots__/automate-simulation-form.spec.js.snap b/app/javascript/spec/automate-simulation-form/__snapshots__/automate-simulation-form.spec.js.snap new file mode 100644 index 00000000000..f9157dc6a45 --- /dev/null +++ b/app/javascript/spec/automate-simulation-form/__snapshots__/automate-simulation-form.spec.js.snap @@ -0,0 +1,606 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Automate Simulation Form should submit a new simulation 1`] = ` +
+
+ ", + "value": "-1", + }, + Object { + "label": "Availability Zone", + "value": "AvailabilityZone", + }, + Object { + "label": "Cloud Network", + "value": "CloudNetwork", + }, + Object { + "label": "Cloud Object Store Container", + "value": "CloudObjectStoreContainer", + }, + Object { + "label": "Cloud Subnet", + "value": "CloudSubnet", + }, + Object { + "label": "Cloud Tenant", + "value": "CloudTenant", + }, + Object { + "label": "Cloud Volume", + "value": "CloudVolume", + }, + Object { + "label": "Container Pod", + "value": "ContainerGroup", + }, + Object { + "label": "Container Image", + "value": "ContainerImage", + }, + Object { + "label": "Container Node", + "value": "ContainerNode", + }, + Object { + "label": "Container Project", + "value": "ContainerProject", + }, + Object { + "label": "Container Template", + "value": "ContainerTemplate", + }, + Object { + "label": "Container Volume", + "value": "ContainerVolume", + }, + Object { + "label": "Cluster", + "value": "EmsCluster", + }, + Object { + "label": "Provider", + "value": "ExtManagementSystem", + }, + Object { + "label": "Generic Object", + "value": "GenericObject", + }, + Object { + "label": "Host", + "value": "Host", + }, + Object { + "label": "Group", + "value": "MiqGroup", + }, + Object { + "label": "VM Template and Image", + "value": "MiqTemplate", + }, + Object { + "label": "Network Router", + "value": "NetworkRouter", + }, + Object { + "label": "Network Service", + "value": "NetworkService", + }, + Object { + "label": "Orchestration Stack", + "value": "OrchestrationStack", + }, + Object { + "label": "Physical Chassis", + "value": "PhysicalChassis", + }, + Object { + "label": "Physical Rack", + "value": "PhysicalRack", + }, + Object { + "label": "Physical Server", + "value": "PhysicalServer", + }, + Object { + "label": "Physical Storage", + "value": "PhysicalStorage", + }, + Object { + "label": "Security Group", + "value": "SecurityGroup", + }, + Object { + "label": "Security Policy", + "value": "SecurityPolicy", + }, + Object { + "label": "Security Policy Rule", + "value": "SecurityPolicyRule", + }, + Object { + "label": "Service", + "value": "Service", + }, + Object { + "label": "Datastore", + "value": "Storage", + }, + Object { + "label": "Virtual Infra Switch", + "value": "Switch", + }, + Object { + "label": "Tenant", + "value": "Tenant", + }, + Object { + "label": "User", + "value": "User", + }, + Object { + "label": "VM and Instance", + "value": "Vm", + }, + ], + "simpleValue": true, + "validate": Array [ + Object { + "condition": Object { + "not": Object { + "or": Array [ + Object { + "is": "-1", + "when": "target_class", + }, + Object { + "isEmpty": true, + "when": "target_class", + }, + ], + }, + }, + "type": "required", + }, + ], + }, + Object { + "className": "automate-selection-target", + "component": "select", + "condition": Object { + "not": Object { + "or": Array [ + Object { + "is": "-1", + "when": "target_class", + }, + Object { + "isEmpty": true, + "when": "target_class", + }, + ], + }, + }, + "id": "selection_target", + "initialValue": null, + "key": "selection_target_-1", + "label": "Selection", + "loadOptions": [Function], + "name": "selection_target", + "validate": Array [ + Object { + "condition": Object { + "not": Object { + "or": Array [ + Object { + "is": "-1", + "when": "target_class", + }, + Object { + "isEmpty": true, + "when": "target_class", + }, + ], + }, + }, + "type": "required", + }, + ], + }, + Object { + "className": "automate-simulation-parameters", + "component": "plain-text", + "id": "simulationParameters", + "label": "Simulation Parameters", + "name": "simulationParameters", + "style": Object { + "fontSize": "16px", + }, + }, + Object { + "className": "automate-readonly", + "component": "checkbox", + "id": "readonly", + "initialValue": true, + "label": "Execute Methods", + "name": "readonly", + "title": "Simulation parameters", + }, + Object { + "component": "plain-text", + "id": "AttributeValuePairs", + "label": "Attribute/Value Pairs", + "name": "AttributeValuePairs", + "style": Object { + "fontSize": "16px", + }, + }, + Array [ + Object { + "className": "subform", + "component": "sub-form", + "fields": Array [ + Object { + "className": "attributeValuePairLabel", + "component": "plain-text", + "id": "attributeValuePairLabel_1", + "label": "1", + "name": "attributeValuePairLabel_1", + "style": Object { + "fontWeight": "bold", + }, + }, + Object { + "component": "text-field", + "fieldprops": Object { + "className": "field-input", + "data-miq_observe": "{\\"interval\\":\\".5\\"}", + }, + "id": "attribute_1", + "initialValue": undefined, + "label": " ", + "maxLength": undefined, + "name": "attribute_1", + }, + Object { + "component": "text-field", + "fieldprops": Object { + "className": "value-input", + "data-miq_observe": "{\\"interval\\":\\".5\\"}", + }, + "id": "value_1", + "initialValue": undefined, + "label": " ", + "maxLength": undefined, + "name": "value_1", + }, + ], + "id": "subform_1", + "name": "subform_1", + }, + ], + Array [ + Object { + "className": "subform", + "component": "sub-form", + "fields": Array [ + Object { + "className": "attributeValuePairLabel", + "component": "plain-text", + "id": "attributeValuePairLabel_2", + "label": "2", + "name": "attributeValuePairLabel_2", + "style": Object { + "fontWeight": "bold", + }, + }, + Object { + "component": "text-field", + "fieldprops": Object { + "className": "field-input", + "data-miq_observe": "{\\"interval\\":\\".5\\"}", + }, + "id": "attribute_2", + "initialValue": undefined, + "label": " ", + "maxLength": undefined, + "name": "attribute_2", + }, + Object { + "component": "text-field", + "fieldprops": Object { + "className": "value-input", + "data-miq_observe": "{\\"interval\\":\\".5\\"}", + }, + "id": "value_2", + "initialValue": undefined, + "label": " ", + "maxLength": undefined, + "name": "value_2", + }, + ], + "id": "subform_2", + "name": "subform_2", + }, + ], + Array [ + Object { + "className": "subform", + "component": "sub-form", + "fields": Array [ + Object { + "className": "attributeValuePairLabel", + "component": "plain-text", + "id": "attributeValuePairLabel_3", + "label": "3", + "name": "attributeValuePairLabel_3", + "style": Object { + "fontWeight": "bold", + }, + }, + Object { + "component": "text-field", + "fieldprops": Object { + "className": "field-input", + "data-miq_observe": "{\\"interval\\":\\".5\\"}", + }, + "id": "attribute_3", + "initialValue": undefined, + "label": " ", + "maxLength": undefined, + "name": "attribute_3", + }, + Object { + "component": "text-field", + "fieldprops": Object { + "className": "value-input", + "data-miq_observe": "{\\"interval\\":\\".5\\"}", + }, + "id": "value_3", + "initialValue": undefined, + "label": " ", + "maxLength": undefined, + "name": "value_3", + }, + ], + "id": "subform_3", + "name": "subform_3", + }, + ], + Array [ + Object { + "className": "subform", + "component": "sub-form", + "fields": Array [ + Object { + "className": "attributeValuePairLabel", + "component": "plain-text", + "id": "attributeValuePairLabel_4", + "label": "4", + "name": "attributeValuePairLabel_4", + "style": Object { + "fontWeight": "bold", + }, + }, + Object { + "component": "text-field", + "fieldprops": Object { + "className": "field-input", + "data-miq_observe": "{\\"interval\\":\\".5\\"}", + }, + "id": "attribute_4", + "initialValue": undefined, + "label": " ", + "maxLength": undefined, + "name": "attribute_4", + }, + Object { + "component": "text-field", + "fieldprops": Object { + "className": "value-input", + "data-miq_observe": "{\\"interval\\":\\".5\\"}", + }, + "id": "value_4", + "initialValue": undefined, + "label": " ", + "maxLength": undefined, + "name": "value_4", + }, + ], + "id": "subform_4", + "name": "subform_4", + }, + ], + Array [ + Object { + "className": "subform", + "component": "sub-form", + "fields": Array [ + Object { + "className": "attributeValuePairLabel", + "component": "plain-text", + "id": "attributeValuePairLabel_5", + "label": "5", + "name": "attributeValuePairLabel_5", + "style": Object { + "fontWeight": "bold", + }, + }, + Object { + "component": "text-field", + "fieldprops": Object { + "className": "field-input", + "data-miq_observe": "{\\"interval\\":\\".5\\"}", + }, + "id": "attribute_5", + "initialValue": undefined, + "label": " ", + "maxLength": undefined, + "name": "attribute_5", + }, + Object { + "component": "text-field", + "fieldprops": Object { + "className": "value-input", + "data-miq_observe": "{\\"interval\\":\\".5\\"}", + }, + "id": "value_5", + "initialValue": undefined, + "label": " ", + "maxLength": undefined, + "name": "value_5", + }, + ], + "id": "subform_5", + "name": "subform_5", + }, + ], + ], + "title": "Object Details", + } + } + /> +
+
+
+ Simulation +
+
+ +
+
+
+`; diff --git a/app/javascript/spec/automate-simulation-form/automate-simulation-form.spec.js b/app/javascript/spec/automate-simulation-form/automate-simulation-form.spec.js new file mode 100644 index 00000000000..05f38bdf7b3 --- /dev/null +++ b/app/javascript/spec/automate-simulation-form/automate-simulation-form.spec.js @@ -0,0 +1,93 @@ +import React from 'react'; +import fetchMock from 'fetch-mock'; +import { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import AutomateSimulationForm from '../../components/automate-simulation-form'; + +describe('Automate Simulation Form', () => { + const automateSimulationMockData = [ + { + href: `/miq_ae_tools/resolve_react/new`, + }, + ]; + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + const resolveMockData = { + ae_result: 'ok', + button_class: '', + button_number: 1, + instance_names: [ + 'Request', 'parse_provider_category', 'parse_event_stream', + 'parse_automation_request', 'MiqEvent', 'GenericObject', 'Event', 'Automation' + ], + lastaction: null, + new: { + attrs: [[], [], [], [], []], + instance_name: 'Request', + object_message: 'create', + object_request: '', + readonly: true, + starting_object: 'SYSTEM/PROCESS', + target_class: null, + target_id: null, + }, + state_attributes: {}, + targets: null, + target_classes: { + AvailabilityZone: 'Availability Zone', + CloudNetwork: 'Cloud Network', + CloudObjectStoreContainer: 'Cloud Object Store Container', + CloudSubnet: 'Cloud Subnet', + CloudTenant: 'Cloud Tenant', + CloudVolume: 'Cloud Volume', + ContainerGroup: 'Container Pod', + ContainerImage: 'Container Image', + ContainerNode: 'Container Node', + ContainerProject: 'Container Project', + ContainerTemplate: 'Container Template', + ContainerVolume: 'Container Volume', + EmsCluster: 'Cluster', + ExtManagementSystem: 'Provider', + GenericObject: 'Generic Object', + Host: 'Host', + MiqGroup: 'Group', + MiqTemplate: 'VM Template and Image', + NetworkRouter: 'Network Router', + NetworkService: 'Network Service', + OrchestrationStack: 'Orchestration Stack', + PhysicalChassis: 'Physical Chassis', + PhysicalRack: 'Physical Rack', + PhysicalServer: 'Physical Server', + PhysicalStorage: 'Physical Storage', + SecurityGroup: 'Security Group', + SecurityPolicy: 'Security Policy', + SecurityPolicyRule: 'Security Policy Rule', + Service: 'Service', + Storage: 'Datastore', + Switch: 'Virtual Infra Switch', + Tenant: 'Tenant', + User: 'User', + Vm: 'VM and Instance', + }, + }; + + it('should submit a new simulation', async() => { + const wrapper = shallow(); + + fetchMock.get(`/miq_ae_tools/resolve_react/new?&expand=resources/`, automateSimulationMockData); + await new Promise((resolve) => { + setImmediate(() => { + wrapper.update(); + expect(toJson(wrapper)).toMatchSnapshot(); + resolve(); + }); + }); + }); +}); diff --git a/app/stylesheet/application-webpack.scss b/app/stylesheet/application-webpack.scss index aa61b6787dc..51aaf564eb8 100644 --- a/app/stylesheet/application-webpack.scss +++ b/app/stylesheet/application-webpack.scss @@ -4,6 +4,7 @@ @import '~@manageiq/ui-components/dist/css/ui-components.css'; @import '~patternfly/dist/css/patternfly.css'; @import '~patternfly/dist/css/patternfly-additions.css'; +@import './automate-simulation.scss'; @import './breadcrumbs.scss'; @import './button-forms.scss'; @import './carbon.scss'; diff --git a/app/stylesheet/automate-simulation.scss b/app/stylesheet/automate-simulation.scss new file mode 100644 index 00000000000..d3df047dcba --- /dev/null +++ b/app/stylesheet/automate-simulation.scss @@ -0,0 +1,102 @@ +.automate-simulation-page { + display: flex; + flex-direction: row; + justify-content: space-between; + + .automate-simulation-form-wrapper { + border-right: 0.5px solid lightgray; + width: 500px; + padding: 25px; + + .automate-object-details, .automate-object-message, + .automate-object-request, .automate-object-attribute, + .automate-selection-target, .automate-readonly, + .automate-simulation-parameters { + margin-bottom: 5%; + } + + .subform { + display: flex; + justify-content: space-between; + align-items: center; + gap: 5%; + } + + .custom-button-wrapper { + display: flex; + width: 100%; + margin-top: 5%; + padding-bottom: 20%; + margin-left: 8% + } + .bx--btn--primary { + margin-right: 8%; + width: 40%; + } + .bx--btn--secondary { + width: 40%; + } + + .attributeValuePairLabel { + display: flex; + justify-content: center; + align-items: center; + margin-top: 10%; + } + } + + .automate-simulation-summary-wrapper { + display: flex; + flex-direction: column; + flex-grow: 1; + padding: 15px; + padding-bottom: 10%; + + .simulation-title-text { + font-size: x-large; + font-weight: lighter; + } + } + + .summary-spinner { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + } + + .flash-message { + display: flex; + align-items: center; + padding: 10px; + margin-bottom: 15px; + background-color: #dff0d8; + opacity: 0.7; + color: black; + border: 1px solid black; + position: relative; + font-weight: bold; + } + + .flash-icon { + font-size: 16px; + font-weight: bold; + color: green; + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 2px solid green; + border-radius: 50%; + margin-right: 2%; + } + + .flash-close { + margin-left: auto; + background: none; + border: none; + font-size: 24px; + cursor: pointer; + } +} \ No newline at end of file diff --git a/app/views/layouts/_ae_resolve_options.html.haml b/app/views/layouts/_ae_resolve_options.html.haml index 5e1a55a5cb5..34708ed11b6 100644 --- a/app/views/layouts/_ae_resolve_options.html.haml +++ b/app/views/layouts/_ae_resolve_options.html.haml @@ -1,123 +1,8 @@ -- field_changed_url ||= "form_field_changed" -- ae_sim_form ||= false -- ae_custom_button ||= false -- ae_ansible_custom_button ||= false - rec_id = @edit && @edit[:action_id].present? ? @edit[:action_id] : "new" -- url = url_for_only_path(:action => field_changed_url, :id => rec_id) -.form - - if form_action == "ae_resolve" && !ae_ansible_custom_button - %h3 - = _("Object Details") - .form-group - %label.control-label - = _("System/Process") - - = select_tag('instance_name', - options_for_select(resolve[:instance_names].sort_by(&:downcase), - resolve[:new][:instance_name]), - "data-miq_sparkle_on" => true, - "data-miq_sparkle_off" => true, - :class => "selectpicker form-control") - :javascript - miqInitSelectPicker(); - miqSelectPickerEvent('instance_name', "#{url}") - - unless ae_ansible_custom_button - .form-group - %label.control-label - = _("Message") - - = text_field_tag("object_message", - resolve[:new][:object_message], - :maxlength => ViewHelper::MAX_NAME_LEN, - :class => "form-control form-control", - "data-miq_observe" => {:interval => '.5', - :url => url}.to_json) - = javascript_tag("if (!$('#description').length) #{javascript_focus('object_message')}") - .form-group - %label.control-label - = _("Request") - - = text_field_tag("object_request", - resolve[:new][:object_request], - :maxlength => ViewHelper::MAX_NAME_LEN, - :class => "form-control form-control", - "data-miq_observe" => {:interval => '.5', :url => url}.to_json) -- if form_action != "miq_action" - - if ae_custom_button - %hr - %h3 - = _("Object Attribute 1") - .form-horizontal - .form-group - %label.control-label - = _("Type") - .col-md-8 - = ui_lookup(:model => @resolve[:target_class]) - - else - %hr - %h3 - = _("Object Attribute") - .form - .form-group - %label.control-label - = _("Type") - - = select_tag('target_class', - options_for_select([["<#{_('None')}>", nil]] + resolve[:target_classes].invert.to_a, - resolve[:new][:target_class]), - "data-miq_sparkle_on" => true, - "data-miq_sparkle_off" => true, - :class => "selectpicker form-control") - :javascript - miqInitSelectPicker(); - miqSelectPickerEvent('target_class', "#{url}") - - if resolve[:new][:target_class] && !resolve[:new][:target_class].blank? && resolve[:targets] - .form-group - %label.control-label - = _("Selection") - - = select_tag('target_id', - options_for_select([["<#{_('Choose')}>", nil]] + resolve[:targets], - resolve[:new][:target_id]), - "data-miq_sparkle_on" => true, - "data-miq_sparkle_off" => true, - :class => "selectpicker form-control") - :javascript - miqInitSelectPicker(); - miqSelectPickerEvent('target_id', "#{url}") -- if ae_sim_form - %hr - %h3 - = _("Simulation Parameters") - .form - .form-group - %label.control-label - = _("Execute Methods") - = check_box_tag("readonly", - "1", - resolve[:new][:readonly] != true, - "data-miq_observe_checkbox" => {:url => url}.to_json) -%hr -%h3 - = _("Attribute/Value Pairs") -.form-horizontal - - ApplicationController::AE_MAX_RESOLUTION_FIELDS.times do |i| - - f = "attribute_" + (i + 1).to_s - - v = "value_" + (i + 1).to_s - .form-group - %label.col-md-2.control-label - = (i + 1).to_s - .col-md-4 - = text_field_tag(f, - resolve[:new][:attrs][i][0], - :maxlength => ViewHelper::MAX_NAME_LEN, - :class => "form-control", - "data-miq_observe" => {:interval => '.5', - :url => url}.to_json) - .col-md-4 - = text_field_tag(v, - resolve[:new][:attrs][i][1], - :maxlength => ViewHelper::MAX_NAME_LEN, - :class => "form-control", - "data-miq_observe" => {:interval => '.5', - :url => url}.to_json) +- url = "/miq_ae_tools/resolve_automate_simulation/#{rec_id}" += react('AutomateSimulationForm', + :resolve => resolve, + :maxNameLength => ViewHelper::MAX_NAME_LEN, + :url => url, + :attrValuesPairs => ApplicationController::AE_MAX_RESOLUTION_FIELDS.times, + :maxLength => ViewHelper::MAX_NAME_LEN) diff --git a/app/views/layouts/_content.html.haml b/app/views/layouts/_content.html.haml index a37b92cc13c..9d0798d0b4b 100644 --- a/app/views/layouts/_content.html.haml +++ b/app/views/layouts/_content.html.haml @@ -12,7 +12,7 @@ = miq_toolbar toolbar_from_hash %main.row.max-height.content-focus-order.responsive-layout-main - if simulate? - #left_div.sidebar-pf.sidebar-pf-left.scrollable.max-height.col-sm-5.col-md-4.col-sm-pull-7.col-md-pull-8 + #left_div.sidebar-pf.scrollable.sidebar-pf-left.max-height.col-md-12 #default_left_cell = yield :left - else @@ -22,37 +22,38 @@ = render :partial => "layouts/listnav" = yield :left #custom_left_cell - .full-content.max-height{:class => simulate? ? 'col-sm-7 col-md-8 col-sm-push-5 col-md-push-4' : 'col-sm-8 col-md-9 col-sm-push-4 col-md-push-3'} - #main-content.row.miq-layout-center_div_with_listnav - .col-md-12 - .row - .col-md-7#explorer - %h1#explorer_title - %span#explorer_title_text - = safe_right_cell_text - -# Link to clear the current applied filter, will be moved via JS to the right cell header - %span#clear_search{:style => "display:none"} - - if route_exists?(:action => 'adv_search_clear') - ( - = link_to(_("clear"), - {:action => "adv_search_clear"}, - "data-miq_sparkle_on" => true, - :remote => true, - "data-method" => :post, - :title => _("Remove the current filter"), - :style => "text-decoration: underline;") - ) - .col-md-5 - %br - = yield :search - .row - .col-md-12 - = yield - .col-md-12.no-padding - = render :partial => 'layouts/x_form_buttons' - .row#paging_div{:style => saved_report_paging? ? "" : "display: none"} - - if saved_report_paging? - = render(:partial => 'layouts/saved_report_paging_bar', :locals => {:pages => @sb[:pages]}) + - if !simulate? + .full-content.max-height{:class => simulate? ? 'col-sm-7 col-md-8 col-sm-push-5 col-md-push-4' : 'col-sm-8 col-md-9 col-sm-push-4 col-md-push-3'} + #main-content.row.miq-layout-center_div_with_listnav + .col-md-12 + .row + .col-md-7#explorer + %h1#explorer_title + %span#explorer_title_text + = safe_right_cell_text + -# Link to clear the current applied filter, will be moved via JS to the right cell header + %span#clear_search{:style => "display:none"} + - if route_exists?(:action => 'adv_search_clear') + ( + = link_to(_("clear"), + {:action => "adv_search_clear"}, + "data-miq_sparkle_on" => true, + :remote => true, + "data-method" => :post, + :title => _("Remove the current filter"), + :style => "text-decoration: underline;") + ) + .col-md-5 + %br + = yield :search + .row + .col-md-12 + = yield + .col-md-12.no-padding + = render :partial => 'layouts/x_form_buttons' + .row#paging_div{:style => saved_report_paging? ? "" : "display: none"} + - if saved_report_paging? + = render(:partial => 'layouts/saved_report_paging_bar', :locals => {:pages => @sb[:pages]}) - elsif layout_full_center = render :partial => layout_full_center - else diff --git a/app/views/miq_ae_tools/_resolve.html.haml b/app/views/miq_ae_tools/_resolve.html.haml index 741d67a497d..d5e5cd57411 100644 --- a/app/views/miq_ae_tools/_resolve.html.haml +++ b/app/views/miq_ae_tools/_resolve.html.haml @@ -1,6 +1,3 @@ - content_for :left do = render :partial => "resolve_form" = render :partial => "resolve_form_buttons" - -#main_div - = render :partial => "results_tabs" diff --git a/app/views/miq_ae_tools/_results_tabs.html.haml b/app/views/miq_ae_tools/_results_tabs.html.haml deleted file mode 100644 index 15b650d4f80..00000000000 --- a/app/views/miq_ae_tools/_results_tabs.html.haml +++ /dev/null @@ -1 +0,0 @@ -= react('AutomationSimulation', {:data => automation_simulation_data(@ae_simulation_tree, @results, @resolve)}) diff --git a/app/views/miq_ae_tools/resolve_react.html.haml b/app/views/miq_ae_tools/resolve_react.html.haml new file mode 100644 index 00000000000..8140df69e7b --- /dev/null +++ b/app/views/miq_ae_tools/resolve_react.html.haml @@ -0,0 +1 @@ += render :partial => "resolve" diff --git a/app/views/shared/buttons/_ab_form.html.haml b/app/views/shared/buttons/_ab_form.html.haml index 3ee6365428b..00d1d918132 100644 --- a/app/views/shared/buttons/_ab_form.html.haml +++ b/app/views/shared/buttons/_ab_form.html.haml @@ -1,25 +1,4 @@ #ab_form - #policy_bar - - if session[:resolve_object].present? - - copied_target_class = session[:resolve_object][:new][:target_class] - - current_target_class = @edit[:new][:target_class] - - if copied_target_class == current_target_class - = link_to({:action => "resolve", :button => "paste"}, - "data-miq_sparkle_on" => true, - "data-miq_sparkle_off" => true, - :remote => true, - "data-method" => :post, - :class => 'btn btn-default', - :title => _("Paste object details for use in a Button.")) do - %i.fa.fa-clipboard - - else - %button.btn.btn-default.disabled{:title => _("Paste is not available, target class differs from the target class of the object copied from the Simulation screen")} - %i.fa.fa-clipboard - - else - %button.btn.btn-default.disabled{:title => _("Paste is not available, no object information has been copied from the Simulation screen")} - %i.fa.fa-clipboard - = render :partial => "layouts/flash_msg" - #custom_button_tabs %ul.nav.nav-tabs{'role' => 'tablist'} = miq_tab_header('ab_options_tab', @sb[:active_tab]) do diff --git a/config/routes.rb b/config/routes.rb index 2056e137c7d..20afeb8acb4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2005,8 +2005,11 @@ fetch_log import_export log + get_form_targets resolve + resolve_automate_simulation review_import + get_simulation_form_vars ], :post => %w[ button @@ -2016,6 +2019,7 @@ import_via_git reset_datastore resolve + resolve_automate_simulation retrieve_git_datastore upload upload_import_file diff --git a/cypress/downloads/downloads.html b/cypress/downloads/downloads.html new file mode 100644 index 0000000000000000000000000000000000000000..2fc3e6692ed7aaf36235d2efb267ae8ef5959c30 GIT binary patch literal 45187 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 Oyq^>Ay;AwV(f Embedded Automate > Simulation', () => { + beforeEach(() => { + cy.login(); + cy.intercept('POST', '/ops/accordion_select?id=rbac_accord').as('accordion'); + cy.menu('Automation', 'Embedded Automate', 'Simulation'); + cy.get('#resolve_form_div'); + }); + + describe('Automate Simulation Form', () => { + it('Resets the form', () => { + cy.get('#object_request').type('Test Request'); + cy.get('#target_class').click(); + cy.get('[class="bx--list-box__menu-item__option"]').contains('Availability Zone').click({force: true}); + + cy.get('#selection_target').select('asia-northeast2-a'); + cy.get('#left_div').scrollTo('bottom'); + cy.contains('button', 'Reset').click(); + + cy.get('#object_request').should('not.contain', 'Test Request'); + cy.get('#target_class').should('have.value', ''); + cy.get('#selection_target').should('not.exist'); + }); + + it('Submits the form', () => { + cy.get('#object_request').type('Test Request'); + cy.get('#target_class').click(); + cy.get('[class="bx--list-box__menu-item__option"]').contains('Availability Zone').click({force: true}); + + cy.get('#selection_target').select('asia-northeast2-a'); + cy.get('#left_div').scrollTo('bottom'); + + cy.get('[name="attribute_1"]').type('attribute 1'); + cy.get('[name="attribute_2"]').type('attribute 2'); + cy.get('[name="attribute_3"]').type('attribute 3'); + cy.get('[name="attribute_4"]').type('attribute 4'); + + cy.get('[name="value_1"]').type('value 1'); + cy.get('[name="value_2"]').type('value 2'); + cy.get('[name="value_3"]').type('value 3'); + cy.get('[name="value_4"]').type('value 4'); + + cy.contains('button', 'Save').click(); + }); + it('Loads the second dropdown', () => { + cy.get('#target_class').click(); + cy.get('[class="bx--list-box__menu-item__option"]').contains('Availability Zone').click({force: true}); + cy.get('#selection_target').should('exist'); + }); + }); +}); diff --git a/spec/helpers/application_helper/buttons/ae_copy_simulate_spec.rb b/spec/helpers/application_helper/buttons/ae_copy_simulate_spec.rb deleted file mode 100644 index ffb21e42e0e..00000000000 --- a/spec/helpers/application_helper/buttons/ae_copy_simulate_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -describe ApplicationHelper::Button::AeCopySimulate do - let(:view_context) { setup_view_context_with_sandbox({}) } - let(:resolve) { {:button_class => button_class} } - let(:button) { described_class.new(view_context, {}, {'resolve' => resolve}, {}) } - - describe '#disabled?' do - context 'when object attribute is specified' do - let(:button_class) { 'some_button_class' } - it_behaves_like 'an enabled button' - end - context 'when object attribute is not specified' do - let(:button_class) { nil } - it_behaves_like 'a disabled button', - 'Object attribute must be specified to copy object details for use in a Button' - end - end -end diff --git a/spec/views/shared/buttons/_ab_form.html.haml_spec.rb b/spec/views/shared/buttons/_ab_form.html.haml_spec.rb deleted file mode 100644 index 663090c9f85..00000000000 --- a/spec/views/shared/buttons/_ab_form.html.haml_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -describe "shared/buttons/_ab_form.html.haml" do - before do - set_controller_for_view("miq_ae_customization") - assign(:sb, :active_tab => "ab_options_tab") - assign(:edit, :new => {:target_class => "CloudNetwork"}) - assign(:resolve, :target_classes => [ - ["Availability Zone", "AvailabilityZone"], - ["Cloud Network", "CloudNetwork"], - ["VM Template and Image", "MiqTemplate"], - ["VM and Instance", "Vm"]]) - stub_template "shared/buttons/_ab_options_form.html.haml" => "" - stub_template "shared/buttons/_ab_advanced_form.html.haml" => "" - end - - describe "Paste button" do - it "is enabled if the copied target class is the same as the current target class" do - allow(view).to receive(:session) - .and_return(:resolve_object => {:new => {:target_class => "CloudNetwork"}}) - render :template => "shared/buttons/_ab_form" - expect(rendered).to include("Paste object details for use in a Button.") - end - - it "is disabled if the copied target class differs from the current target class" do - allow(view).to receive(:session) - .and_return(:resolve_object => {:new => {:target_class => "AvailabilityZone"}}) - render :template => "shared/buttons/_ab_form" - expect(rendered).to include("Paste is not available, target class differs from the target class of the object copied from the Simulation screen") - end - end -end