diff --git a/app/controllers/miq_ae_class_controller.rb b/app/controllers/miq_ae_class_controller.rb
index bb7da5ba57c..21bb1e9c02c 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
@@ -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,49 @@ 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
+ 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]}
+ edit_hash[:current] = if params[:old_data]
+ {:name => params[:old_data][:name],
+ :display_name => params[:old_data][:display_name],
+ :description => params[:old_data][:description]}
+ else
+ {: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 (
+
+ );
+};
+
+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');
+ });
+ });
+});