diff --git a/app/controllers/miq_ae_class_controller.rb b/app/controllers/miq_ae_class_controller.rb index d13ce6bb009..b5cae74d3cf 100644 --- a/app/controllers/miq_ae_class_controller.rb +++ b/app/controllers/miq_ae_class_controller.rb @@ -804,6 +804,25 @@ def ae_class_for_instance_or_method(record) record.id ? record.ae_class : MiqAeClass.find(x_node.split("-").last) end + def validate_automate_method_data + assert_privileges("miq_ae_method_edit") + @edit[:new][:data] = params[:cls_method_data] if params[:cls_method_data] + @edit[:new][:data] = params[:method_data] if params[:method_data] + res = MiqAeMethod.validate_syntax(@edit[:new][:data]) + line = 0 + if !res + render :json => {:status => true, :message => _("Data validated successfully")} + else + res.each do |err| + line = err[0] if line.zero? + render :json => { + :status => false, + :message => (_("Error on line %{line_num}: %{err_txt}") % {:line_num => err[0], :err_txt => err[1]}) + } + end + end + end + def validate_method_data assert_privileges("miq_ae_method_edit") return unless load_edit("aemethod_edit__#{params[:id]}", "replace_cell__explorer") diff --git a/app/javascript/components/automate-method-form/automate-method-code-mirror/index.jsx b/app/javascript/components/automate-method-form/automate-method-code-mirror/index.jsx new file mode 100644 index 00000000000..1495f76f943 --- /dev/null +++ b/app/javascript/components/automate-method-form/automate-method-code-mirror/index.jsx @@ -0,0 +1,66 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { Button, Accordion, AccordionItem } from 'carbon-components-react'; +import { Controlled as CodeMirror } from 'react-codemirror2'; +import { http } from '../../../http_api'; +import NotificationMessage from '../../notification-message'; +import AutomateMethodContext from '../automate-method-context'; + +const AutomateMethodCodeMirror = () => { + const { updateCodeEditor } = useContext(AutomateMethodContext); + + const defaultEditorContents = `#\n# Description: \n#\n`; + + const [data, setData] = useState({ + editorContents: defaultEditorContents, + enableValidationButton: false, + validation: undefined, + }); + + useEffect(() => { + updateCodeEditor(data.editorContents); + }, [data.validation]); + + const validate = () => { + const formData = { cls_method_data: data.editorContents }; + http.post('/miq_ae_class/validate_automate_method_data/new?button=validate', formData).then((response) => { + setData({ + ...data, + validation: response, + }); + }); + }; + + const renderValidateButton = () => ( +
+ +
+ ); + + return ( +
+ + + { + data.validation && + } + setData({ ...data, validation: undefined, editorContents: value })} + value={data.editorContents} + /> + {renderValidateButton()} + + +
+ ); +}; + +export default AutomateMethodCodeMirror; diff --git a/app/javascript/components/automate-method-form/automate-method-context.js b/app/javascript/components/automate-method-form/automate-method-context.js new file mode 100644 index 00000000000..c764f944b4f --- /dev/null +++ b/app/javascript/components/automate-method-form/automate-method-context.js @@ -0,0 +1,5 @@ +import { createContext } from 'react'; + +const AutomateMethodContext = createContext(); + +export default AutomateMethodContext; diff --git a/app/javascript/components/automate-method-form/automate-method-input-parameter/automate-method-input-parameter-form.jsx b/app/javascript/components/automate-method-form/automate-method-input-parameter/automate-method-input-parameter-form.jsx new file mode 100644 index 00000000000..bdae7861012 --- /dev/null +++ b/app/javascript/components/automate-method-form/automate-method-input-parameter/automate-method-input-parameter-form.jsx @@ -0,0 +1,53 @@ +import React, { useState, useEffect, useContext } from 'react'; +import MiqFormRenderer from '@@ddf'; +import { Modal } from 'carbon-components-react'; +import PropTypes from 'prop-types'; +import { inputParameterSchema } from './schema'; +import { InputParameterRecordActions } from './helper'; +import AutomateMethodContext from '../automate-method-context'; + +const AutomateMethodInputParameterForm = ({ modalStatus }) => { + /** Context to access data from parent component */ + const { formData, updateInputParameter } = useContext(AutomateMethodContext); + + const [data, setData] = useState({ + initialValues: undefined, + }); + + /** Effect hook to update initial values when selectedId changes */ + useEffect(() => { + const { selectedId, items } = formData.inputParameter; + if (selectedId) { + setData({ + ...data, + initialValues: items[selectedId], + }); + } + }, [formData.inputParameter.selectedId]); + + const addOrUpdateInputParameter = (values) => (formData.inputParameter.selectedId + ? updateInputParameter(InputParameterRecordActions.UPDATE, { values }) + : updateInputParameter(InputParameterRecordActions.ADD, { values })); + + return ( + updateInputParameter(InputParameterRecordActions.CLOSE, undefined)} + passiveModal + > + addOrUpdateInputParameter(values)} + /> + + ); +}; + +export default AutomateMethodInputParameterForm; + +AutomateMethodInputParameterForm.propTypes = { + modalStatus: PropTypes.bool.isRequired, +}; diff --git a/app/javascript/components/automate-method-form/automate-method-input-parameter/helper.js b/app/javascript/components/automate-method-form/automate-method-input-parameter/helper.js new file mode 100644 index 00000000000..b3f89be1082 --- /dev/null +++ b/app/javascript/components/automate-method-form/automate-method-input-parameter/helper.js @@ -0,0 +1,102 @@ +/** Action buttons for the input parameter table */ +export const InputParameterButtonActions = { + EDIT: 'editInputParameter', + DELETE: 'deleteInputParameter', +}; + +export const InputParameterRecordActions = { + OPEN: 'openModal', + CLOSE: 'closeModal', + ADD: 'add', + UPDATE: 'update', + DELETE: 'delete', +}; + +const editInputParameterButton = () => ({ + is_button: true, + title: __('Edit'), + text: __('Edit'), + alt: __('Edit'), + kind: 'ghost', + callback: InputParameterButtonActions.EDIT, +}); + +const deleteInputParameterButton = () => ({ + is_button: true, + title: __('Delete'), + text: __('Delete'), + alt: __('Delete'), + kind: 'ghost', + callback: InputParameterButtonActions.DELETE, +}); + +/** Input parameter data for table */ +export const reformatList = (items) => items.map((item, index) => ({ + ...item, + id: index.toString(), + edit: editInputParameterButton(item, index), + delete: deleteInputParameterButton(item, index), +})); + +export const headers = [ + { key: 'inputName', header: __('Input Name') }, + { key: 'dataType', header: __('Data Type') }, + { key: 'defaultValue', header: __('Default Value') }, + { key: 'edit', header: __('Edit') }, + { key: 'delete', header: __('Delete') }, +]; + +/* Function to handle the action buttons */ +export const handleInputParameterUpdate = (actionType, data, formData) => { + const { inputParameter } = formData; + + if (actionType !== InputParameterRecordActions.DELETE) { + inputParameter.modal = false; + } + + switch (actionType) { + case InputParameterRecordActions.OPEN: + inputParameter.modal = true; + if (data && data.selectedId) { + inputParameter.selectedId = data.selectedId; + } + break; + case InputParameterRecordActions.CLOSE: + inputParameter.modal = false; + inputParameter.selectedId = undefined; + break; + case InputParameterRecordActions.ADD: + inputParameter.items.push(data.values); + inputParameter.selectedId = undefined; + break; + case InputParameterRecordActions.UPDATE: + inputParameter.items[inputParameter.selectedId] = data.values; + inputParameter.selectedId = undefined; + break; + case InputParameterRecordActions.DELETE: + inputParameter.items.splice(data.selectedId, 1); + inputParameter.selectedId = undefined; + break; + default: + console.warn(__('Unknown action')); + } + + return { ...formData.inputParameter }; +}; + +/** Helper function to get provider details and restructure its options */ +export const initialState = { + manager_id: null, +}; + +export const reducer = (state, action) => { + switch (action.type) { + case 'SET_MANAGER_ID': + return { + ...state, + manager_id: action.payload, + }; + default: + return state; + } +}; diff --git a/app/javascript/components/automate-method-form/automate-method-input-parameter/index.jsx b/app/javascript/components/automate-method-form/automate-method-input-parameter/index.jsx new file mode 100644 index 00000000000..75c46258467 --- /dev/null +++ b/app/javascript/components/automate-method-form/automate-method-input-parameter/index.jsx @@ -0,0 +1,66 @@ +import React, { useContext } from 'react'; +import { Button, Accordion, AccordionItem } from 'carbon-components-react'; +import { + InputParameterButtonActions, InputParameterRecordActions, headers, reformatList, +} from './helper'; +import MiqDataTable from '../../miq-data-table'; +import NotificationMessage from '../../notification-message'; +import AutomateMethodContext from '../automate-method-context'; + +const AutomateMethodInputParameter = () => { + /** Context to access data from parent component */ + const { formData, updateInputParameter } = useContext(AutomateMethodContext); + + /** Input parameter selection handler */ + const onSelect = (item) => { + if (item && item.callbackAction) { + switch (item.callbackAction) { + case InputParameterButtonActions.EDIT: + return updateInputParameter(InputParameterRecordActions.OPEN, { selectedId: item.id }); + case InputParameterButtonActions.DELETE: + return updateInputParameter(InputParameterRecordActions.DELETE, { selectedId: item.id }); + default: + return undefined; + } + } + return undefined; + }; + + const renderAddButton = () => ( +
+ +
+ + ); + + return ( +
+ + + {renderAddButton()} + { + formData.inputParameter.items.length > 0 + ? ( + onSelect(selectedRow)} + mode="button-group-list" + /> + ) + : ( + <> +
+ + + ) + } +
+
+
+ ); +}; + +export default AutomateMethodInputParameter; diff --git a/app/javascript/components/automate-method-form/automate-method-input-parameter/schema.js b/app/javascript/components/automate-method-form/automate-method-input-parameter/schema.js new file mode 100644 index 00000000000..e61d4b3fe65 --- /dev/null +++ b/app/javascript/components/automate-method-form/automate-method-input-parameter/schema.js @@ -0,0 +1,33 @@ +/* eslint-disable camelcase */ +import { componentTypes, validatorTypes } from '@@ddf'; + +/** Schema for input parameter form */ +export const inputParameterSchema = ({ available_datatypes }) => ({ + fields: [ + { + component: componentTypes.TEXT_FIELD, + id: 'inputName', + name: 'inputName', + label: __('Input Name'), + isRequired: true, + validate: [{ type: validatorTypes.REQUIRED }], + }, + { + component: componentTypes.SELECT, + id: 'dataType', + name: 'dataType', + label: __('Choose'), + options: available_datatypes.map((item) => ({ label: item, value: item })), + isRequired: true, + validate: [{ type: validatorTypes.REQUIRED }], + }, + { + component: componentTypes.TEXT_FIELD, + id: 'defaultValue', + name: 'defaultValue', + label: __('Default Value'), + isRequired: true, + validate: [{ type: validatorTypes.REQUIRED }], + }, + ], +}); diff --git a/app/javascript/components/automate-method-form/index.jsx b/app/javascript/components/automate-method-form/index.jsx new file mode 100644 index 00000000000..eeaf9198b85 --- /dev/null +++ b/app/javascript/components/automate-method-form/index.jsx @@ -0,0 +1,169 @@ +import React, { useState, useEffect } from 'react'; +import { Dropdown, Loading } from 'carbon-components-react'; +import PropTypes from 'prop-types'; +import MiqFormRenderer from '@@ddf'; +import { createSchema } from './schema'; +import { handleInputParameterUpdate } from './automate-method-input-parameter/helper'; +import componentMapper from '../../forms/mappers/componentMapper'; +import AutomateMethodInputParameter from './automate-method-input-parameter'; +import AutomateMethodCodeMirror from './automate-method-code-mirror'; +import AutomateMethodInputParameterForm from './automate-method-input-parameter/automate-method-input-parameter-form'; +import AutomateMethodContext from './automate-method-context'; +import { http } from '../../http_api'; +import './style.scss'; + +const AutomateMethodForm = ({ availableLocations, levels }) => { + const mapper = { + ...componentMapper, + 'automate-method-code-mirror': AutomateMethodCodeMirror, + 'automate-method-input-parameter': AutomateMethodInputParameter, + }; + + const [formData, setFormData] = useState({ + loading: false, + apiResponse: undefined, + selectedType: undefined, + manager_id: undefined, + workflowTemplates: undefined, + levels, + codeEditor: undefined, + inputParameter: { + modal: false, + selectedId: undefined, + items: [], + }, + }); + + /** Fetching data based on selected type */ + useEffect(() => { + if (formData.selectedType && formData.selectedType.id) { + http.get(`/miq_ae_class/method_form_fields/new?location=${formData.selectedType.id}`).then((apiResponse) => { + setFormData({ + ...formData, + loading: false, + apiResponse, + }); + }); + } + }, [formData.selectedType]); + + /** Fetching templates based on manager_id */ + useEffect(() => { + if (formData.manager_id) { + const collectionClass = 'collection_class=ManageIQ::Providers::AnsibleTower::AutomationManager::ConfigurationScript'; + const filter = `filter[]=manager_id=${formData.manager_id}`; + const sort = `sort_by=name&sort_order=asc`; + const url = `/api/configuration_scripts?expand=resources&${collectionClass}&${filter}&${sort}`; + API.get(url).then((response) => { + miqSparkleOn(); + setFormData({ + ...formData, + workflowTemplates: response, + }); + miqSparkleOff(); + }); + } else { + setFormData({ + ...formData, + workflowTemplates: undefined, + }); + } + }, [formData.manager_id]); + + /** Function to update input parameters */ + const updateInputParameter = (actionType, data) => { + setFormData({ + ...formData, + inputParameter: handleInputParameterUpdate(actionType, data, formData), + }); + }; + + /** Function to render code editor */ + const updateCodeEditor = (data) => { + setFormData({ + ...formData, + codeEditor: data, + }); + }; + + const onSubmit = (values) => { + let extraVars; + if (formData.inputParameter.items) { + extraVars = formData.inputParameter.items.map((item) => Object.values(item)); + } + console.log({ ...values, extra_vars: extraVars }); + }; + + /** Function to render automate types dropdown */ + const renderAutomateTypes = () => ( +
+

{__('Main Info')}

+ ({ id: item[1], label: item[0] }))} + itemToString={(item) => (item ? item.label : '')} + onChange={({ selectedItem }) => setFormData({ + ...formData, + loading: true, + selectedType: selectedItem, + })} + titleText={formData.selectedType ? formData.selectedType.label : ''} + /> +
+ ); + + /** Function to render input parameter modal */ + const renderInputParameterModal = ({ inputParameter }) => ( + + ); + + const renderLoader = () => ( +
+ +
+ ); + + const renderFormContents = () => ( + <> + { + formData.selectedType && ( + + + { + formData.inputParameter.modal && renderInputParameterModal(formData) + } + + ) + } + + ); + + return ( +
+ { + renderAutomateTypes() + } + { + formData.loading + ? renderLoader() + : renderFormContents() + } +
+ ); +}; + +export default AutomateMethodForm; + +AutomateMethodForm.propTypes = { + availableLocations: PropTypes.arrayOf(PropTypes.any).isRequired, + levels: PropTypes.shape({}).isRequired, +}; diff --git a/app/javascript/components/automate-method-form/schema.config.js b/app/javascript/components/automate-method-form/schema.config.js new file mode 100644 index 00000000000..2a305f397f9 --- /dev/null +++ b/app/javascript/components/automate-method-form/schema.config.js @@ -0,0 +1,173 @@ +import { componentTypes } from '@@ddf'; + +const dropdownOptions = (items) => items.map((item) => ({ label: item.name, value: item.id })); + +export const ansibleFields = (formData, setFormData) => ([ + { + component: componentTypes.SELECT, + id: 'manager_id', + name: 'manager_id', + label: __('Provider'), + placeholder: __(''), + includeEmpty: true, + options: formData.apiResponse && formData.apiResponse.managers ? dropdownOptions(formData.apiResponse.managers) : [], + onChange: (managerId) => { + setFormData({ + ...formData, + // eslint-disable-next-line radix + manager_id: parseInt(managerId), + }); + }, + }, + { + component: componentTypes.SELECT, + id: 'ansible_template_id', + name: 'ansible_template_id', + label: __('Workflow Template'), + placeholder: __(''), + includeEmpty: true, + options: (formData.workflowTemplates && formData.workflowTemplates.resources) + ? dropdownOptions(formData.workflowTemplates.resources) + : [], + }, +]); + +const ansibleFieldsCommon = ({ levels }) => ([ + { + component: componentTypes.TEXT_FIELD, + id: 'execution_ttl', + name: 'execution_ttl', + label: __('Max TTL(mins)'), + }, + { + component: componentTypes.SELECT, + id: 'log_output', + name: 'log_output', + label: __('Logging Output'), + options: Object.entries(levels.output).map(([id, name]) => ({ label: name, value: id })), + }, +]); + +const additionalFields = [ + { + id: 'host', + component: componentTypes.RADIO, + label: __('Hosts'), + name: 'host', + options: [ + { value: 'localhost', label: 'Localhost' }, + { value: 'specify', label: 'Specify host values' }, + ], + }, + { + component: componentTypes.TEXTAREA, + name: 'provisioning_inventory', + label: __('Specify details'), + condition: { + and: [{ when: 'host', is: 'specify' }], + }, + }, +]; + +const builtInFields = [ + { + component: componentTypes.TEXT_FIELD, + id: 'cls_method_data', + name: 'cls_method_data', + label: __('Builtin name'), + helperText: 'Optional, if not specified, method name is used', + }, +]; + +const expressionFields = [ + { + component: componentTypes.SELECT, + id: 'cls_exp_object', + name: 'cls_exp_object', + label: __('Expression Object'), + options: [], + }, + { + component: componentTypes.TEXTAREA, + id: 'editexpression', + name: 'editexpression', + label: __('Placeholder For Edit Expression'), + }, + { + component: componentTypes.TEXTAREA, + id: 'editselected', + name: 'editselected', + label: __('Placeholder For Edit Selected Element'), + }, +]; + +const playBookFields = [ + { + component: componentTypes.SELECT, + id: 'repository', + name: 'repository', + label: __('Repository'), + placeholder: __(''), + options: [], + }, + { + component: componentTypes.SELECT, + id: 'playbook', + name: 'playbook', + label: __('PlayBook'), + condition: { + when: 'repository', + isNotEmpty: true, + }, + }, + { + component: componentTypes.SELECT, + id: 'machineCredential', + name: 'machineCredential', + label: __('Machine Credential'), + condition: { + when: 'repository', + isNotEmpty: true, + }, + }, + { + component: componentTypes.SELECT, + id: 'vaultCredential', + name: 'vaultCredential', + label: __('Vault Credential'), + condition: { + when: 'repository', + isNotEmpty: true, + }, + }, + { + component: componentTypes.SELECT, + id: 'cloudType', + name: 'cloudType', + label: __('Cloud Type'), + condition: { + when: 'repository', + isNotEmpty: true, + }, + }, +]; + +const verbosityField = ({ levels }) => ([ + { + component: componentTypes.SELECT, + id: 'verbosity', + name: 'verbosity', + label: __('Verbosity'), + placeholder: __(''), + includeEmpty: true, + options: Object.entries(levels.verbosity).map(([value, label]) => ({ label, value })), + }, +]); + +export const schemaConfig = (formData, setFormData) => ({ + ansibleJobTemplate: [...ansibleFields(formData, setFormData), ...additionalFields, ...(ansibleFieldsCommon(formData))], + ansibleWorkflowTemplate: [...ansibleFields(formData, setFormData), ...ansibleFieldsCommon(formData)], + builtIn: [...builtInFields], + expression: [...expressionFields], + playbook: [...playBookFields, ...additionalFields, ...ansibleFieldsCommon(formData), ...verbosityField(formData)], +}); diff --git a/app/javascript/components/automate-method-form/schema.js b/app/javascript/components/automate-method-form/schema.js new file mode 100644 index 00000000000..03317b7496b --- /dev/null +++ b/app/javascript/components/automate-method-form/schema.js @@ -0,0 +1,95 @@ +import { componentTypes } from '@@ddf'; +import { schemaConfig } from './schema.config'; + +const commonFields = [ + { + component: componentTypes.TEXT_FIELD, + id: 'type', + name: 'type', + label: __('Type'), + }, + { + component: componentTypes.TEXT_FIELD, + id: 'fully-qualified-name', + name: 'fully-qualified-name', + label: __('Fully Qualified Name'), + initialValue: '', + }, + { + component: componentTypes.TEXT_FIELD, + id: 'name', + name: 'name', + label: __('Name'), + initialValue: '', + }, + { + component: componentTypes.TEXT_FIELD, + id: 'display_name', + name: 'display_name', + label: __('Display Name'), + initialValue: '', + }, +]; + +const inputParametersField = [ + { + component: 'automate-method-input-parameter', + id: 'inputParameter', + name: 'inputParameter', + label: __('Input Parameter'), + }, +]; + +const codeMirrorField = [ + { + component: 'automate-method-code-mirror', + id: 'codeMirror', + name: 'codeMirror', + label: __('Data'), + }, +]; + +const automateFields = (conditionalFields) => [ + ...commonFields, + ...conditionalFields, + ...inputParametersField, +]; + +export const createSchema = (formData, setFormData) => { + let selectedFields = []; + const { selectedType } = formData; + const config = schemaConfig(formData, setFormData); + + switch (selectedType.id) { + case 'ansible_job_template': + selectedFields = automateFields(config.ansibleJobTemplate); + break; + case 'ansible_workflow_template': + selectedFields = automateFields(config.ansibleWorkflowTemplate); + break; + case 'builtin': + selectedFields = automateFields(config.builtIn); + break; + case 'expression': + selectedFields = automateFields(config.expression); + break; + case 'inline': + selectedFields = [...commonFields, ...codeMirrorField, ...inputParametersField]; + break; + case 'playbook': + selectedFields = automateFields(config.playbook); + break; + default: + selectedFields = []; + } + return { + fields: [ + { + component: componentTypes.SUB_FORM, + id: 'name-wrapper', + name: 'subform-1', + fields: selectedFields, + }, + ], + }; +}; diff --git a/app/javascript/components/automate-method-form/style.scss b/app/javascript/components/automate-method-form/style.scss new file mode 100644 index 00000000000..5542f8827fa --- /dev/null +++ b/app/javascript/components/automate-method-form/style.scss @@ -0,0 +1,32 @@ +.custom-form-wrapper { + border: 1px solid lightgray; + margin-bottom: 20px; + display: flex; + flex-direction: column; + + .miq-custom-form-accordion + { + border: 1px solid #e0e0e0; + + li button.bx--accordion__heading { + background: #e0e0e0; + } + .bx--accordion__item:last-child{ + border: 0; + } + + .bx--accordion__content { + padding: 20px; + margin: 0; + + .custom-form-buttons { + display: flex; + justify-content: flex-end; + } + + .ae-inline-methods-notification { + margin-top: 20px; + } + } + } +} diff --git a/app/javascript/packs/component-definitions-common.js b/app/javascript/packs/component-definitions-common.js index 6b7e9f572a7..24d934aebd9 100644 --- a/app/javascript/packs/component-definitions-common.js +++ b/app/javascript/packs/component-definitions-common.js @@ -18,6 +18,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 AutomateMethodForm from '../components/automate-method-form' import AutomationSimulation from '../components/AutomationSimulation'; import ButtonList from '../components/data-tables/button-list'; import ButtonGroupList from '../components/data-tables/button-group-list'; @@ -187,6 +188,7 @@ ManageIQ.component.addReact('AnsiblePlaybookWorkflow', AnsiblePlaybookWorkflow); ManageIQ.component.addReact('AnsibleRepositoryForm', AnsibleRepositoryForm); ManageIQ.component.addReact('AttachDetachCloudVolumeForm', AttachDetachCloudVolumeForm); ManageIQ.component.addReact('AuthKeypairCloudForm', AuthKeypairCloudForm); +ManageIQ.component.addReact('AutomateMethodForm', AutomateMethodForm); ManageIQ.component.addReact('AutomationSimulation', AutomationSimulation); ManageIQ.component.addReact('BreadcrumbsBar', BreadcrumbsBar); ManageIQ.component.addReact('ButtonList', ButtonList); diff --git a/app/stylesheet/miq-data-table.scss b/app/stylesheet/miq-data-table.scss index f6181bdd3c1..8bdaf93eb4c 100644 --- a/app/stylesheet/miq-data-table.scss +++ b/app/stylesheet/miq-data-table.scss @@ -240,7 +240,7 @@ vertical-align: top; } - thead tr th { + thead tr th { vertical-align: middle; .bx--table-header-label { @@ -299,7 +299,7 @@ table.miq_preview { width: 100px !important; } -.reconfigure-form { +.reconfigure-form, .automate-method-form { .reconfigure-sub-form { display: flex; gap: 20px; @@ -310,7 +310,7 @@ table.miq_preview { .form-section-title { display: flex; - h3 { + h3 { flex-grow: 1; } .reconfigure-add-button { @@ -374,7 +374,7 @@ table.miq_preview { display: flex; align-items: center; } - + .workflows-entry-point-modal-body { .miq-data-table { margin-top: 0px; diff --git a/app/views/miq_ae_class/_method_form.html.haml b/app/views/miq_ae_class/_method_form.html.haml index ac7576ddf1a..7c2953dc588 100644 --- a/app/views/miq_ae_class/_method_form.html.haml +++ b/app/views/miq_ae_class/_method_form.html.haml @@ -1,6 +1,10 @@ - if @sb[:active_tab] == "methods" - url = url_for_only_path(:action => 'form_method_field_changed', :id => (@ae_method.id || 'new')) - obs = {:interval => '.5', :url => url}.to_json + +- levels = {:output => ViewHelper::LOG_OUTPUT_LEVELS, :verbosity => ViewHelper::VERBOSITY_LEVELS} += react('AutomateMethodForm', availableLocations: available_locations_with_labels, levels: levels) + %h3 = _('Main Info') .form-horizontal diff --git a/config/routes.rb b/config/routes.rb index 7a9b745a340..869c2aae9f0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1948,6 +1948,7 @@ update_method update_namespace validate_method_data + validate_automate_method_data x_button x_history x_show