From 73c00d512bea3731a1083e00aa2c3413d3b1368c Mon Sep 17 00:00:00 2001 From: Elsa Mary Date: Wed, 30 Oct 2024 16:45:36 +0530 Subject: [PATCH] Transforms haml forms to react for Automate class --- app/controllers/miq_ae_class_controller.rb | 67 ++++++- .../miq-ae-class/class-form.schema.js | 36 ++++ .../components/miq-ae-class/index.jsx | 174 ++++++++++++++++++ .../packs/component-definitions-common.js | 2 + .../miq-ae-class-form.spec.js.snap | 5 + .../miq-ae-class-form.spec.js | 62 +++++++ app/views/miq_ae_class/_class_form.html.haml | 41 +---- config/routes.rb | 2 + .../Embedded-Automate/Explorer/class.cy.js | 125 +++++++++++++ 9 files changed, 478 insertions(+), 36 deletions(-) create mode 100644 app/javascript/components/miq-ae-class/class-form.schema.js create mode 100644 app/javascript/components/miq-ae-class/index.jsx create mode 100644 app/javascript/spec/miq-ae-class-form/__snapshots__/miq-ae-class-form.spec.js.snap create mode 100644 app/javascript/spec/miq-ae-class-form/miq-ae-class-form.spec.js create mode 100644 cypress/e2e/ui/Automation/Embedded-Automate/Explorer/class.cy.js diff --git a/app/controllers/miq_ae_class_controller.rb b/app/controllers/miq_ae_class_controller.rb index bb7da5ba57c..c7751289d04 100644 --- a/app/controllers/miq_ae_class_controller.rb +++ b/app/controllers/miq_ae_class_controller.rb @@ -341,6 +341,9 @@ def replace_right_cell(options = {}) :serialize => @sb[:active_tab] == 'methods', } ]) + if @hide_bottom_bar + presenter.hide(:paging_div, :form_buttons_div) + end else # incase it was hidden for summary screen, and incase there were no records on show_list presenter.hide(:paging_div, :form_buttons_div) @@ -459,6 +462,7 @@ def edit_class @ae_class = find_record_with_rbac(MiqAeClass, params[:id]) end set_form_vars + @hide_bottom_bar = true # have to get name and set node info, to load multiple tabs correctly # rec_name = get_rec_name(@ae_class) # get_node_info("aec-#{@ae_class.id}") @@ -468,6 +472,24 @@ def edit_class replace_right_cell end + def edit_class_record + assert_privileges("miq_ae_class_edit") + unless params[:id] + obj = find_checked_items + @_params[:id] = obj[0] + end + @hide_bottom_bar = true + + class_rec = MiqAeClass.find(params[:id]) + + render :json => { + :fqname => class_rec.fqname, + :name => class_rec.name, + :display_name => class_rec.display_name, + :description => class_rec.description + } + end + def edit_fields assert_privileges("miq_ae_field_edit") if params[:pressed] == "miq_ae_item_edit" # came from Namespace details screen @@ -1073,7 +1095,7 @@ def method_form_fields def update assert_privileges("miq_ae_class_edit") return unless load_edit("aeclass_edit__#{params[:id]}", "replace_cell__explorer") - + get_form_vars @changed = (@edit[:new] != @edit[:current]) case params[:button] @@ -1291,6 +1313,7 @@ def new assert_privileges("miq_ae_class_new") @ae_class = MiqAeClass.new set_form_vars + @hide_bottom_bar = true @in_a_form = true replace_right_cell end @@ -1841,8 +1864,50 @@ def ae_method_operations end end + def class_update + assert_privileges(params[:id].present? ? 'miq_ae_class_edit' : 'miq_ae_class_new') + @hide_bottom_bar = true + id = params[:id] ? params[:id] : "new" + class_update_create + end + private + def class_update_create + case params[:button] + when "add", "save" + class_rec = params[:id].blank? ? MiqAeClass.new : MiqAeClass.find(params[:id]) # Get new or existing record + add_flash(_("Name is required"), :error) if params[:name].blank? + class_rec.name = params[:name] + class_rec.display_name = params[:display_name] + class_rec.description = params[:description] + class_rec.namespace_id = x_node.split('-')[1] if params[:id].blank? + begin + class_rec.save! + rescue StandardError + class_rec.errors.each do |error| + add_flash("#{error.attribute.to_s.capitalize} #{error.message}", :error) + end + @changed = true + javascript_flash + else + edit_hash = {} + edit_hash[:new] = {:name => params[:name], + :display_name => params[:display_name], :description => params[:description]} + if params[:old_data] + edit_hash[:current] = {:name => params[:old_data][:name], + :display_name => params[:old_data][:display_name], + :description => params[:old_data][:description]} + else + edit_hash[:current] = {:name => nil, :display_name => nil, :description => nil} + end + AuditEvent.success(build_saved_audit(class_rec, edit_hash)) + @edit = session[:edit] = nil # clean out the saved info + replace_right_cell(:nodetype => x_node, :replace_trees => [:ae]) + end + end + end + def get_template_class(location) if location == "ansible_workflow_template" ManageIQ::Providers::ExternalAutomationManager::ConfigurationWorkflow diff --git a/app/javascript/components/miq-ae-class/class-form.schema.js b/app/javascript/components/miq-ae-class/class-form.schema.js new file mode 100644 index 00000000000..49133416e82 --- /dev/null +++ b/app/javascript/components/miq-ae-class/class-form.schema.js @@ -0,0 +1,36 @@ +import { componentTypes, validatorTypes } from '@@ddf'; + +const createSchema = (fqname) => ({ + fields: [ + { + component: componentTypes.PLAIN_TEXT, + name: 'fqname', + label: `${__('Fully Qualified Name')}:\t ${fqname}`, + }, + { + component: componentTypes.TEXT_FIELD, + id: 'name', + name: 'name', + label: __('Name'), + maxLength: 128, + validate: [{ type: validatorTypes.REQUIRED }], + isRequired: true, + }, + { + component: componentTypes.TEXT_FIELD, + id: 'display_name', + name: 'display_name', + label: __('Display Name'), + maxLength: 128, + }, + { + component: componentTypes.TEXT_FIELD, + id: 'description', + name: 'description', + label: __('Description'), + maxLength: 255, + }, + ], +}); + +export default createSchema; diff --git a/app/javascript/components/miq-ae-class/index.jsx b/app/javascript/components/miq-ae-class/index.jsx new file mode 100644 index 00000000000..c5138cdbcbd --- /dev/null +++ b/app/javascript/components/miq-ae-class/index.jsx @@ -0,0 +1,174 @@ +import React, { useState, useEffect } from 'react'; +import { FormSpy } from '@data-driven-forms/react-form-renderer'; +import { Button } from 'carbon-components-react'; +import MiqFormRenderer, { useFormApi } from '@@ddf'; +import PropTypes from 'prop-types'; +import createSchema from './class-form.schema'; +import miqRedirectBack from '../../helpers/miq-redirect-back'; + +const MiqAeClass = ({ classRecord, fqname }) => { + const [data, setData] = useState({ + isLoading: true, + initialValues: undefined, + }); + + const isEdit = !!(classRecord && classRecord.id); + + useEffect(() => { + if (isEdit) { + http.get(`/miq_ae_class/edit_class_record/${classRecord.id}/`).then((recordValues) => { + if (recordValues) { + setData({ ...data, isLoading: false, initialValues: recordValues }); + } + }); + } else { + const initialValues = { + fqname, + name: classRecord && classRecord.name, + display_name: classRecord && classRecord.display_name, + description: classRecord && classRecord.description, + }; + setData({ ...data, isLoading: false, initialValues }); + } + }, [classRecord]); + + const onSubmit = (values) => { + miqSparkleOn(); + + const params = { + action: isEdit ? 'edit' : 'create', + name: values.name, + display_name: values.display_name, + description: values.description, + old_data: data.initialValues, + button: classRecord.id ? 'save' : 'add', + }; + + const request = isEdit + ? http.post(`/miq_ae_class/class_update/${classRecord.id}`, params) + : http.post(`/miq_ae_class/class_update/`, params); + + request + .then(() => { + const confirmation = isEdit ? __(`Class "${values.name}" was saved`) : __(`Class "${values.name}" was added`); + miqRedirectBack(sprintf(confirmation, values.name), 'success', '/miq_ae_class/explorer'); + }) + .catch(miqSparkleOff); + }; + + const onCancel = () => { + const confirmation = classRecord.id ? __(`Edit of Class "${classRecord.name}" cancelled by the user`) + : __(`Add of new Class was cancelled by the user`); + const message = sprintf( + confirmation + ); + miqRedirectBack(message, 'warning', '/miq_ae_class/explorer'); + }; + + return (!data.isLoading + ? ( +
+ {}} + FormTemplate={(props) => } + /> +
+ ) : null + ); +}; + +const FormTemplate = ({ + formFields, recId, +}) => { + const { + handleSubmit, onReset, onCancel, getState, + } = useFormApi(); + const { valid, pristine } = getState(); + const submitLabel = !!recId ? __('Save') : __('Add'); + return ( +
+ {formFields} + + {() => ( +
+ { !recId + ? ( + + ) : ( + + )} + {!!recId + ? ( + + ) : null} + + +
+ )} +
+
+ ); +}; + +MiqAeClass.propTypes = { + classRecord: PropTypes.shape({ + id: PropTypes.number, + name: PropTypes.string, + display_name: PropTypes.string, + description: PropTypes.string, + }), + fqname: PropTypes.string.isRequired, +}; + +MiqAeClass.defaultProps = { + classRecord: undefined, +}; + +FormTemplate.propTypes = { + formFields: PropTypes.arrayOf( + PropTypes.shape({ id: PropTypes.number }), + PropTypes.shape({ name: PropTypes.string }), + PropTypes.shape({ display_name: PropTypes.string }), + PropTypes.shape({ description: PropTypes.string }), + ), + recId: PropTypes.number, +}; + +FormTemplate.defaultProps = { + formFields: undefined, + recId: undefined, +}; + +export default MiqAeClass; diff --git a/app/javascript/packs/component-definitions-common.js b/app/javascript/packs/component-definitions-common.js index f56709a0b77..b5e3b5de67f 100644 --- a/app/javascript/packs/component-definitions-common.js +++ b/app/javascript/packs/component-definitions-common.js @@ -177,6 +177,7 @@ import WorkflowPayload from '../components/workflows/workflow_payload'; import WorkflowRepositoryForm from '../components/workflow-repository-form'; import XmlHolder from '../components/XmlHolder'; import ZoneForm from '../components/zone-form'; +import MiqAeClass from '../components/miq-ae-class'; /** * Add component definitions to this file. @@ -363,3 +364,4 @@ ManageIQ.component.addReact('WorkflowPayload', WorkflowPayload); ManageIQ.component.addReact('WorkflowRepositoryForm', WorkflowRepositoryForm); ManageIQ.component.addReact('XmlHolder', XmlHolder); ManageIQ.component.addReact('ZoneForm', ZoneForm); +ManageIQ.component.addReact('MiqAeClass', MiqAeClass); diff --git a/app/javascript/spec/miq-ae-class-form/__snapshots__/miq-ae-class-form.spec.js.snap b/app/javascript/spec/miq-ae-class-form/__snapshots__/miq-ae-class-form.spec.js.snap new file mode 100644 index 00000000000..412578bfe67 --- /dev/null +++ b/app/javascript/spec/miq-ae-class-form/__snapshots__/miq-ae-class-form.spec.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MiqAeClass Form Component should render add class form correctly 1`] = `""`; + +exports[`MiqAeClass Form Component should render edit class form correctly 1`] = `""`; diff --git a/app/javascript/spec/miq-ae-class-form/miq-ae-class-form.spec.js b/app/javascript/spec/miq-ae-class-form/miq-ae-class-form.spec.js new file mode 100644 index 00000000000..8e369f9327b --- /dev/null +++ b/app/javascript/spec/miq-ae-class-form/miq-ae-class-form.spec.js @@ -0,0 +1,62 @@ +import React from 'react'; +import fetchMock from 'fetch-mock'; +import { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import MiqAeClass from '../../components/miq-ae-class'; + +describe('MiqAeClass Form Component', () => { + const classMockData = [ + { + href: `/miq_ae_class/edit_class/2/`, + id: 2, + description: 'Configured System Provision', + }, + ]; + + const MiqAeClassEditData = { + id: 40, + name: 'test', + display_name: 'test display name', + description: 'test description', + }; + + const fqName = 'Sample FQ Name'; + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + it('should render add class form correctly', async() => { + const wrapper = shallow(); + + fetchMock.get(`/miq_ae_class/new?&expand=resources/`, classMockData); + + await new Promise((resolve) => { + setImmediate(() => { + wrapper.update(); + expect(toJson(wrapper)).toMatchSnapshot(); + resolve(); + }); + }); + }); + + it('should render edit class form correctly', async() => { + const wrapper = shallow(); + + fetchMock.get(`/miq_ae_class/edit_class_react/${MiqAeClassEditData.id}?&expand=resources/`, classMockData); + await new Promise((resolve) => { + setImmediate(() => { + wrapper.update(); + expect(toJson(wrapper)).toMatchSnapshot(); + resolve(); + }); + }); + }); +}); diff --git a/app/views/miq_ae_class/_class_form.html.haml b/app/views/miq_ae_class/_class_form.html.haml index 5d3d9bc3808..a3e5041d203 100644 --- a/app/views/miq_ae_class/_class_form.html.haml +++ b/app/views/miq_ae_class/_class_form.html.haml @@ -1,35 +1,6 @@ -- url = url_for_only_path(:action => 'form_field_changed', :id => (@ae_class.id || 'new')) -- obs = {:interval => '.5', :url => url}.to_json -%h3 - = _('Properties') -.form-horizontal - .form-group - %label.col-md-2.control-label - = _('Fully Qualified Name') - .col-md-8 - = @sb[:namespace_path] - .form-group - %label.col-md-2.control-label - = _('Name') - .col-md-8 - = text_field_tag("name", @edit[:new][:name], - :maxlength => ViewHelper::MAX_NAME_LEN, - :class => "form-control", - "data-miq_observe" => obs) - = javascript_tag(javascript_focus('name')) - .form-group - %label.col-md-2.control-label - = _('Display Name') - .col-md-8 - = text_field_tag("display_name", @edit[:new][:display_name], - :maxlength => ViewHelper::MAX_NAME_LEN, - :class => "form-control", - "data-miq_observe" => obs) - .form-group - %label.col-md-2.control-label - = _('Description') - .col-md-8 - = text_field_tag("description", @edit[:new][:description], - :maxlength => ViewHelper::MAX_NAME_LEN, - :class => "form-control", - "data-miq_observe" => obs) +- if @ae_class + - if @in_a_form + = react('MiqAeClass', + :classRecord => @ae_class, + :fqname => @sb[:namespace_path]) + diff --git a/config/routes.rb b/config/routes.rb index 2056e137c7d..a796ad5f217 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1911,6 +1911,7 @@ ae_methods ae_method_operations show + edit_class_record ], :post => %w[ add_update_method @@ -1955,6 +1956,7 @@ x_button x_history x_show + class_update ] + adv_search_post + exp_post }, diff --git a/cypress/e2e/ui/Automation/Embedded-Automate/Explorer/class.cy.js b/cypress/e2e/ui/Automation/Embedded-Automate/Explorer/class.cy.js new file mode 100644 index 00000000000..d728adf6824 --- /dev/null +++ b/cypress/e2e/ui/Automation/Embedded-Automate/Explorer/class.cy.js @@ -0,0 +1,125 @@ +/* eslint-disable no-undef */ + +describe('Automation > Embedded Automate > Explorer', () => { + beforeEach(() => { + cy.login(); + cy.intercept('POST', '/ops/accordion_select?id=rbac_accord').as('accordion'); + cy.menu('Automation', 'Embedded Automate', 'Explorer'); + cy.get('#explorer_title_text'); + }); + + afterEach(() => { + // Remove Domain after each tests + cy.get('[title="Datastore"]').click({force: true}); + cy.get('[title="Automate Domain: TestDomain"]').click({force: true}); + cy.get('[title="Configuration"]').click({force: true}); + cy.get('[title="Remove this Domain"]').click({force: true}); + + cy.get('.bx--data-table-content tbody tr').should('not.contain', 'Automate Domain: TestDomain'); + }); + + describe('Class Form', () => { + it('Creates and edits an automate class', () => { + // Creates a Domain + cy.get('[title="Datastore"]').click({force: true}); + cy.get('[title="Configuration"]').click({force: true}); + cy.get('[title="Add a New Domain"]').click({force: true}); + cy.get('[name="name"]').type('TestDomain'); + cy.get('[name="description"]').type('This is a test domain'); + cy.get('#enabled').check(); + cy.get('[class="bx--btn bx--btn--primary"]').contains('Add').click(); // submits Domain + // checks for the success message + cy.get('div.alert.alert-success.alert-dismissable') + .should('exist') + .and('contain', 'Automate Domain "TestDomain" was added') + .find('button.close').should('exist'); + + // Creates a Namespace + cy.get('[title="Datastore"]').click({force: true}); + cy.get('[title="Automate Domain: TestDomain"]').click({force: true}); // clicks on Domain + cy.get('[title="Configuration"]').click({force: true}); + cy.get('[title="Add a New Namespace"]').click({force: true}); + cy.get('[name="name"]').type('TestNS'); + cy.get('[name="description"]').type('This is a test NS'); + cy.get('.bx--btn--primary').contains('Add').click(); // submits Namespace + + // Creates a Class + cy.get('[title="Datastore"]').click({force: true}); + cy.get('[title="Automate Domain: TestDomain"]').click({force: true}); // clicks on Domain + cy.get('[title="Automate Namespace: TestNS"]').click({force: true}); // clicks on Namespace + cy.get('[title="Configuration"]').click({force: true}); + cy.get('[title="Add a New Class"]').click({force: true}); + cy.get('[name="name"]').type('TestClass'); + cy.get('[name="display_name"]').type('TC'); + cy.get('[name="description"').type('This is a test class desc'); + cy.get('.bx--btn--primary').contains('Add').click(); // submits class + // checks for the success message + cy.get('#flash_msg_div .alert.alert-success').should('exist') + .and('be.visible').and('contain', 'Class "TestClass" was added'); + + // Edits a class + cy.get('[title="Automate Class: TC (TestClass)"]').click({force: true}); // clicks on the class + cy.get('[title="Configuration"]').click({force: true}); + cy.get('[title="Edit this Class"]').click({force: true}); + cy.get('[name="display_name"]').clear({force: true}); + cy.get('[name="display_name"]').type('Edited TC', {force: true}); + cy.get('[name="description"').clear({force: true}); + cy.get('[name="description"').type('Edited Test Class Description'); + cy.get('[class="btnRight bx--btn bx--btn--primary"]').contains('Save').click({force: true}); + // Checks if class data was updated + cy.get('#props_tab a').click(); // Navigate to the Properties tab + cy.get('div.label_header:contains("Name")').siblings('.content_value') + .should('contain', 'TestClass'); + cy.get('div.label_header:contains("Display Name")').siblings('.content_value') + .should('contain', 'Edited TC'); + cy.get('div.label_header:contains("Description")').siblings('.content_value') + .should('contain', 'Edited Test Class Description'); + + // Clicks the Cancel button during class creation + cy.get('[title="Datastore"]').click({force: true}); + cy.get('[title="Automate Domain: TestDomain"]').click({force: true}); + cy.get('[title="Automate Namespace: TestNS"]').click({force: true}); + cy.get('[title="Configuration"]').click({force: true}); + cy.get('[title="Add a New Class"]').click({force: true}); + cy.get('[class="bx--btn bx--btn--secondary"]') + .contains('Cancel').click({force: true}); // clicks Cancel button + cy.get('[id="explorer_title_text"]').contains('Automate Namespace "TestNS"'); + + // Clicks the Cancel button during class update + cy.get('[title="Automate Class: Edited TC (TestClass)"]').click({force: true}); // clicks on the class + cy.get('[title="Configuration"]').click({force: true}); + cy.get('[title="Edit this Class"]').click({force: true}); + cy.get('[name="description"]').clear({force: true}); + cy.get('[name="description"]').type('New description for class', {force: true}); + cy.get('[class="bx--btn bx--btn--secondary"]').contains('Cancel').click({force: true}); + // Checks if class data was updated + cy.get('#props_tab a').click(); // Navigate to the Properties tab + cy.get('div.label_header:contains("Description")').siblings('.content_value') + .should('not.contain', 'New description for class') + .should('contain', 'Edited Test Class Description'); + + // Clicks the Reset button during class update + cy.get('[title="Automate Class: Edited TC (TestClass)"]').click({force: true}); // clicks on the class + cy.get('[title="Configuration"]').click({force: true}); + cy.get('[title="Edit this Class"]').click({force: true}); + cy.get('[name="description"]').clear({force: true}); + cy.get('[name="description"]').type('New description for class', {force: true}); + cy.get('[class="btnRight bx--btn bx--btn--secondary"]').contains('Reset').click({force: true}); + // Check for the flash message div + cy.get('#flash_msg_div .alert.alert-warning').should('exist') + .and('be.visible').and('contain', 'All changes have been reset'); + // Checks if class data was updated + cy.get('[name="description"]').should('have.value', 'Edited Test Class Description'); + cy.get('[class="bx--btn bx--btn--secondary"]').contains('Cancel').click({force: true}); + + // Removes class + cy.get('[title="Automate Class: Edited TC (TestClass)"]').click({force: true}); // clicks on the class + cy.get('[title="Configuration"]').click({force: true}); + cy.get('[title="Remove this Class"]').click({force: true}); + // checks for the success message + cy.get('div.alert.alert-success.alert-dismissable') + .should('exist') + .and('contain', 'Automate Class "TestClass": Delete successful'); + }); + }); +});