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 (
+
+ );
+};
+
+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",
+ }
+ }
+ />
+
+
+
+`;
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