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 00000000000..2fc3e6692ed Binary files /dev/null and b/cypress/downloads/downloads.html differ diff --git a/cypress/e2e/ui/Embedded-Automate/simulation.cy.js b/cypress/e2e/ui/Embedded-Automate/simulation.cy.js new file mode 100644 index 00000000000..ff60d758879 --- /dev/null +++ b/cypress/e2e/ui/Embedded-Automate/simulation.cy.js @@ -0,0 +1,52 @@ +/* eslint-disable no-undef */ + +describe('Automation > 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