diff --git a/app/controllers/miq_ae_class_controller.rb b/app/controllers/miq_ae_class_controller.rb index d13ce6bb0096..e07b3b647704 100644 --- a/app/controllers/miq_ae_class_controller.rb +++ b/app/controllers/miq_ae_class_controller.rb @@ -1815,6 +1815,18 @@ def namespace render :json => find_record_with_rbac(MiqAeNamespace, params[:id]).attributes.slice('name', 'description', 'enabled') end + def ae_domains + domains = MiqAeDomain.where("ancestry is null and enabled = ?", true).order("name").select("name,id") + render :json => {:domains => domains} + end + + def ae_methods + methods = MiqAeMethod.all.order('name') + methods = methods.where('name ILIKE ? or relative_path ILIKE ?', "%#{params[:search]}%", "%#{params[:search]}%") if params[:search] + methods = methods.where('domain_id = ?', params[:domain_id]) if params[:domain_id] + render :json => {:methods => methods} + end + private def feature_by_action diff --git a/app/javascript/components/AeInlineMethod/NamespaceSelector.jsx b/app/javascript/components/AeInlineMethod/NamespaceSelector.jsx new file mode 100644 index 000000000000..f58f3ffc5600 --- /dev/null +++ b/app/javascript/components/AeInlineMethod/NamespaceSelector.jsx @@ -0,0 +1,143 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { + TextInput, Select, SelectItem, Loading, +} from 'carbon-components-react'; +import MiqDataTable from '../miq-data-table'; +import NotificationMessage from '../notification-message'; +import './style.scss'; + +/** Component to search and select AeMethods. */ +const NamespaceSelector = ({ onSelectMethod }) => { + const aeMethodsUrl = '/miq_ae_class/ae_methods'; + const aeDomainsUrl = '/miq_ae_class/ae_domains'; + + const [data, setData] = useState({ + loading: true, + domains: [], + searchText: undefined, + methods: [], + selectedDomain: undefined, + }); + + /** Function to format the method data needed for the data-table list. */ + const formatMethods = (methods) => (methods.map((item) => ({ + id: item.id.toString(), + name: { text: item.name, icon: 'icon node-icon fa-ruby' }, + path: item.relative_path, + code: item.data, + }))); + + /** Loads the 'domains' and 'methods' from its respective URL's during the component's onLoad event. */ + useEffect(() => { + Promise.all([ + http.get(aeDomainsUrl), + http.get(aeMethodsUrl)]) + .then(([{ domains }, { methods }]) => { + setData({ + ...data, + loading: false, + methods: formatMethods(methods), + domains, + }); + }); + }, []); + + /** Headers needed for the data-table list. */ + const miqHeaders = [ + { + key: 'name', + header: 'Name', + }, + { + key: 'path', + header: 'Relative path', + }, + ]; + + /** Function to return a conditional URL based on the selected filters. */ + const searchUrl = (selectedDomain, text) => { + const queryParams = []; + if (selectedDomain) { + queryParams.push(`domain_id=${selectedDomain}`); + } + if (text) { + queryParams.push(`search=${text}`); + } + const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : ''; + return `${aeMethodsUrl}${queryString}`; + }; + + /** Function to handle search text and drop-down item onchange events. */ + const handleInputChange = (text, selectedDomain) => { + const url = searchUrl(selectedDomain, text); + http.get(url) + .then(({ methods }) => { + setData({ + ...data, + methods: formatMethods(methods), + searchText: text, + selectedDomain, + }); + }); + }; + + /** Function to render the search text. */ + const renderSearchText = () => ( + handleInputChange(event.target.value, data.selectedDomain)} + /> + ); + + /** Function to render the domain items in a drop-down list. */ + const renderDomainList = () => ( + + ); + + /** Function to render the contents of the list. */ + const renderContents = () => (data.methods && data.methods.length > 0 + ? ( + onSelectMethod(selectedRow)} + /> + ) + : ); + + return ( +
+
+ {renderSearchText()} + {data.domains && renderDomainList()} +
+
+ { + data.loading + ? + : renderContents() + } +
+
+ ); +}; + +export default NamespaceSelector; + +NamespaceSelector.propTypes = { + onSelectMethod: PropTypes.func.isRequired, +}; diff --git a/app/javascript/components/AeInlineMethod/index.jsx b/app/javascript/components/AeInlineMethod/index.jsx new file mode 100644 index 000000000000..0ef4ad55c86c --- /dev/null +++ b/app/javascript/components/AeInlineMethod/index.jsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, Button, ModalBody } from 'carbon-components-react'; +import NamespaceSelector from './NamespaceSelector'; + +/** Component to render a tree and to select an embedded method. */ +const AeInlineMethod = ({ type }) => { + const [data, setData] = useState({ + isModalOpen: false, + selectedNode: undefined, + list: [], + }); + + /** Function to show/hide the modal. */ + const showModal = (status) => { + setData({ + ...data, + isModalOpen: status, + }); + }; + + /** Function to render the Add method button. */ + const renderAddButton = () => ( + + ); + + return ( +
+ {renderAddButton()} + 0} + size="lg" + modalHeading={__('Select item')} + open={data.isModalOpen} + primaryButtonText={__('OK')} + secondaryButtonText={__('Cancel')} + onRequestClose={() => showModal(false)} + onRequestSubmit={() => { + console.log('on onRequestSubmit', data.list); + showModal(false); + }} + onSecondarySubmit={() => { + console.log('on onSecondarySubmit'); + showModal(false); + }} + > + + { + data.isModalOpen + && ( + { + data.list.push(method); + setData({ + ...data, + list: [...data.list], + }); + }} + /> + ) + } + + + +
+ ); +}; + +export default AeInlineMethod; + +AeInlineMethod.propTypes = { + type: PropTypes.string.isRequired, +}; diff --git a/app/javascript/components/AeInlineMethod/style.scss b/app/javascript/components/AeInlineMethod/style.scss new file mode 100644 index 000000000000..0e63cf75c3d6 --- /dev/null +++ b/app/javascript/components/AeInlineMethod/style.scss @@ -0,0 +1,25 @@ +.inline-method-selector { + display: flex; + flex-direction: column; + + .inline-filters { + display: flex; + flex-direction: row; + gap: 10px; + } + + .inline-contents-wrapper { + display: flex; + flex-direction: column; + margin-top: 20px; + + .miq-inline-method-list { + background: #FFF; + margin-top: 0; + } + + .miq-notification-message-container { + margin: 0; + } + } +} diff --git a/app/javascript/packs/component-definitions-common.js b/app/javascript/packs/component-definitions-common.js index 6b7e9f572a73..6b4149b414d4 100644 --- a/app/javascript/packs/component-definitions-common.js +++ b/app/javascript/packs/component-definitions-common.js @@ -11,6 +11,7 @@ import { Toolbar } from '../components/toolbar'; import ActionForm from '../components/action-form'; import AddRemoveHostAggregateForm from '../components/host-aggregate-form/add-remove-host-aggregate-form'; import AddRemoveSecurityGroupForm from '../components/vm-cloud-add-remove-security-group-form'; +import AeInlineMethod from '../components/AeInlineMethod'; import AggregateStatusCard from '../components/aggregate_status_card'; import AnsibleCredentialsForm from '../components/ansible-credentials-form'; import AnsiblePlayBookEditCatalogForm from '../components/ansible-playbook-edit-catalog-form'; @@ -181,6 +182,7 @@ ManageIQ.component.addReact('ActionForm', ActionForm); ManageIQ.component.addReact('AddRemoveHostAggregateForm', AddRemoveHostAggregateForm); ManageIQ.component.addReact('AddRemoveSecurityGroupForm', AddRemoveSecurityGroupForm); ManageIQ.component.addReact('AggregateStatusCard', AggregateStatusCard); +ManageIQ.component.addReact('AeInlineMethod', AeInlineMethod); ManageIQ.component.addReact('AnsibleCredentialsForm', AnsibleCredentialsForm); ManageIQ.component.addReact('AnsiblePlayBookEditCatalogForm', AnsiblePlayBookEditCatalogForm); ManageIQ.component.addReact('AnsiblePlaybookWorkflow', AnsiblePlaybookWorkflow); diff --git a/app/views/miq_ae_class/_class_instances.html.haml b/app/views/miq_ae_class/_class_instances.html.haml index c4b0b3e0ebae..57a9b2042622 100644 --- a/app/views/miq_ae_class/_class_instances.html.haml +++ b/app/views/miq_ae_class/_class_instances.html.haml @@ -1,3 +1,5 @@ += react('AeInlineMethod', {:type => "aeInlineMethod"}) + #class_instances_div - unless @angular_form - if !@in_a_form diff --git a/config/routes.rb b/config/routes.rb index 01db17d40095..09f33016f111 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1906,6 +1906,8 @@ explorer method_form_fields namespace + ae_domains + ae_methods show ], :post => %w[