diff --git a/app/controllers/miq_ae_class_controller.rb b/app/controllers/miq_ae_class_controller.rb index e07b3b647704..88ee6612602a 100644 --- a/app/controllers/miq_ae_class_controller.rb +++ b/app/controllers/miq_ae_class_controller.rb @@ -1816,14 +1816,15 @@ def namespace end def ae_domains - domains = MiqAeDomain.where("ancestry is null and enabled = ?", true).order("name").select("name,id") + domains = MiqAeDomain.where("ancestry is null and enabled = ?", true).order("name").select("id, name") render :json => {:domains => domains} end def ae_methods - methods = MiqAeMethod.all.order('name') + methods = MiqAeMethod.all.order('name').select("id, relative_path, 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] + methods = methods.where(id: params[:ids].split(',').map(&:to_i)) if params[:ids] render :json => {:methods => methods} end diff --git a/app/javascript/components/AeInlineMethod/FilterNamespace.jsx b/app/javascript/components/AeInlineMethod/FilterNamespace.jsx new file mode 100644 index 000000000000..c66aa1137c6a --- /dev/null +++ b/app/javascript/components/AeInlineMethod/FilterNamespace.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { TextInput, Select, SelectItem } from 'carbon-components-react'; +import { noSelect } from './helper'; + +const FilterNamespace = ({ domains, filterOnChange }) => { + /** Function to render the search text. */ + const renderSearchText = () => ( + filterOnChange({ text: event.target.value || noSelect })} + /> + ); + + /** Function to render the domain items in a drop-down list. */ + const renderDomainList = () => ( + + ); + + return ( +
+ {renderSearchText()} + {domains && renderDomainList()} +
+ ); +}; + +export default FilterNamespace; + +FilterNamespace.propTypes = { + domains: PropTypes.arrayOf(PropTypes.any), + filterOnChange: PropTypes.func.isRequired, +}; + +FilterNamespace.defaultProps = { + domains: undefined, +}; diff --git a/app/javascript/components/AeInlineMethod/NamespaceSelector.jsx b/app/javascript/components/AeInlineMethod/NamespaceSelector.jsx index f58f3ffc5600..bb1ed61d8b74 100644 --- a/app/javascript/components/AeInlineMethod/NamespaceSelector.jsx +++ b/app/javascript/components/AeInlineMethod/NamespaceSelector.jsx @@ -1,130 +1,82 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { - TextInput, Select, SelectItem, Loading, -} from 'carbon-components-react'; +import { Loading } from 'carbon-components-react'; +import FilterNamespace from './FilterNamespace'; import MiqDataTable from '../miq-data-table'; import NotificationMessage from '../notification-message'; +import { CellAction } from '../miq-data-table/helper'; +import { + methodSelectorHeaders, formatMethods, searchUrl, namespaceUrls, +} from './helper'; 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 NamespaceSelector = ({ onSelectMethod, selectedIds }) => { const [data, setData] = useState({ - loading: true, domains: [], - searchText: undefined, methods: [], + loading: true, + searchText: undefined, 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)]) + http.get(namespaceUrls.aeDomainsUrl), + http.get(namespaceUrls.aeMethodsUrl)]) .then(([{ domains }, { methods }]) => { setData({ ...data, + domains, 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 handleFilterOnChange = (filterData) => { + const text = filterData.text ? filterData.text : data.searchText; + const selectedDomain = filterData.selectedDomain ? filterData.selectedDomain : data.selectedDomain; const url = searchUrl(selectedDomain, text); http.get(url) .then(({ methods }) => { setData({ ...data, - methods: formatMethods(methods), - searchText: text, selectedDomain, + searchText: text, + methods: formatMethods(methods), }); }); }; - /** 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 handle the click events for the list. */ + const onCellClickHandler = (selectedRow, cellType, checked) => { + const selectedItems = cellType === CellAction.selectAll + ? data.methods.map((item) => item.id) + : [selectedRow]; + onSelectMethod({ selectedItems, cellType, checked }); + }; /** Function to render the contents of the list. */ const renderContents = () => (data.methods && data.methods.length > 0 ? ( onSelectMethod(selectedRow)} + rowCheckBox + sortable={false} + gridChecks={selectedIds} + onCellClick={(selectedRow, cellType, event) => onCellClickHandler(selectedRow, cellType, event.target.checked)} /> ) : ); return (
-
- {renderSearchText()} - {data.domains && renderDomainList()} -
+ handleFilterOnChange(filterData)} />
{ data.loading @@ -140,4 +92,5 @@ export default NamespaceSelector; NamespaceSelector.propTypes = { onSelectMethod: PropTypes.func.isRequired, + selectedIds: PropTypes.arrayOf(PropTypes.any).isRequired, }; diff --git a/app/javascript/components/AeInlineMethod/helper.js b/app/javascript/components/AeInlineMethod/helper.js new file mode 100644 index 000000000000..8db2f338e414 --- /dev/null +++ b/app/javascript/components/AeInlineMethod/helper.js @@ -0,0 +1,59 @@ +export const namespaceUrls = { + aeMethodsUrl: '/miq_ae_class/ae_methods', + aeDomainsUrl: '/miq_ae_class/ae_domains', +}; + +export const noSelect = 'NONE'; + +/** Headers needed for the data-table list. */ +export const methodSelectorHeaders = [ + { + key: 'name', + header: 'Name', + }, + { + key: 'path', + header: 'Relative path', + }, +]; + +export const methodListHeaders = [ + ...methodSelectorHeaders, + { key: 'delete', header: __('Delete') }, +]; + +/** Function to format the method data needed for the data-table list. */ +export const formatMethods = (methods) => (methods.map((item) => ({ + id: item.id.toString(), + name: { text: item.name, icon: 'icon node-icon fa-ruby' }, + path: item.relative_path, +}))); + +const deleteMethodButton = () => ({ + is_button: true, + title: __('Delete'), + text: __('Delete'), + alt: __('Delete'), + kind: 'ghost', + callback: 'removeMethod', +}); + +export const formatListMethods = (methods) => (methods.map((item, index) => ({ + id: item.id.toString(), + name: { text: item.name, icon: 'icon node-icon fa-ruby' }, + path: item.relative_path, + delete: deleteMethodButton(item, index), +}))); + +/** Function to return a conditional URL based on the selected filters. */ +export const searchUrl = (selectedDomain, text) => { + const queryParams = []; + if (selectedDomain && selectedDomain !== noSelect) { + queryParams.push(`domain_id=${selectedDomain}`); + } + if (text && text !== noSelect) { + queryParams.push(`search=${text}`); + } + const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : ''; + return `${namespaceUrls.aeMethodsUrl}${queryString}`; +}; diff --git a/app/javascript/components/AeInlineMethod/index.jsx b/app/javascript/components/AeInlineMethod/index.jsx index 0ef4ad55c86c..dac2ab085e4e 100644 --- a/app/javascript/components/AeInlineMethod/index.jsx +++ b/app/javascript/components/AeInlineMethod/index.jsx @@ -1,75 +1,152 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { Modal, Button, ModalBody } from 'carbon-components-react'; +import NotificationMessage from '../notification-message'; +import MiqDataTable from '../miq-data-table'; import NamespaceSelector from './NamespaceSelector'; +import { CellAction } from '../miq-data-table/helper'; +import { formatListMethods, methodListHeaders, namespaceUrls } from './helper'; /** Component to render a tree and to select an embedded method. */ const AeInlineMethod = ({ type }) => { const [data, setData] = useState({ isModalOpen: false, - selectedNode: undefined, - list: [], + selectedIds: [], + rows: [], }); + useEffect(() => { + if (!data.isModalOpen) { + if (data.selectedIds.length > 0) { + http.get(`${namespaceUrls.aeMethodsUrl}?ids=${data.selectedIds.map((str) => parseInt(str, 10))}`) + .then(({ methods }) => { + setData({ ...data, rows: formatListMethods(methods) }); + }); + } else { + setData({ ...data, rows: [] }); + } + } + }, [data.isModalOpen]); + /** Function to show/hide the modal. */ - const showModal = (status) => { + const showModal = (status) => setData({ ...data, isModalOpen: status }); + + /** Function to handle the select-all check-box click event. */ + const onSelectAll = (selectedItems, checked) => { setData({ ...data, - isModalOpen: status, + selectedIds: checked ? [...selectedItems] : [], }); }; - /** Function to render the Add method button. */ - const renderAddButton = () => ( - - ); + /** Function to handle the list row selection events. + * selectedItem is passed as an array. */ + const onItemSelect = (selectedItem, checked) => { + if (checked) { + data.selectedIds.push(selectedItem[0].id); + } else { + data.selectedIds = data.selectedIds.filter((item) => item !== selectedItem[0].id); + } + setData({ + ...data, + selectedIds: [...data.selectedIds], + }); + }; - 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 + /** Function to add/remove an selected items. */ + const onSelectMethod = (selectedItems, cellType, checked) => { + switch (cellType) { + case CellAction.selectAll: onSelectAll(selectedItems, checked); break; + default: onItemSelect(selectedItems, checked); break; + } + }; + + /** Function to handle the click events for the list. */ + const onCellClickHandler = (item) => { + if (item && item.callbackAction) { + if (item.callbackAction === 'removeMethod') { + setData({ + rows: data.rows.filter((row) => row.id !== item.id), + selectedIds: data.selectedIds.filter((id) => id !== item.id), + }); + } + } + }; + + /** Function to render the modal with namespace selector component. */ + const renderModalSelector = () => ( + showModal(false)} + onRequestSubmit={() => showModal(false)} + onSecondarySubmit={() => showModal(false)} + > + + { + data.isModalOpen && ( { - data.list.push(method); - setData({ - ...data, - list: [...data.list], - }); - }} + onSelectMethod={({ selectedItems, cellType, checked }) => onSelectMethod(selectedItems, cellType, checked)} + selectedIds={data.selectedIds} /> ) - } - - + } + + + ); + /** Function to render the contents of the list. */ + const renderList = () => (data.rows && data.rows.length > 0 + ? ( + onCellClickHandler(selectedRow)} + /> + ) + : ( + <> +
+ + + )); + + const renderAddButton = () => ( +
+ +
+ ); + + /** Function to render the Add method button. */ + const renderCustomContents = () => ( + <> +
+
{__('Methods')}
+
+
+ {renderAddButton()} + {renderList()} +
+ + ); + return ( +
+ {renderCustomContents()} + {renderModalSelector()}
); }; diff --git a/app/javascript/components/AeInlineMethod/style.scss b/app/javascript/components/AeInlineMethod/style.scss index 0e63cf75c3d6..b51253fa62b9 100644 --- a/app/javascript/components/AeInlineMethod/style.scss +++ b/app/javascript/components/AeInlineMethod/style.scss @@ -23,3 +23,32 @@ } } } + +.custom-form-wrapper { + border: 1px solid lightgray; + margin-bottom: 20px; + display: flex; + flex-direction: column; + + .custom-form-title-wrapper { + background: lightgray; + display: flex; + flex-direction: row; + + .custom-form-title { + display: flex; + flex-grow: 1; + font-size: 16px; + padding: 8px 10px; + } + } + + .custom-form-component-wrapper { + padding: 10px; + + .custom-form-buttons { + display: flex; + justify-content: flex-end; + } + } +} diff --git a/app/javascript/components/miq-data-table/miq-table-cell.jsx b/app/javascript/components/miq-data-table/miq-table-cell.jsx index c22ef0e5cd05..d661e72de42e 100644 --- a/app/javascript/components/miq-data-table/miq-table-cell.jsx +++ b/app/javascript/components/miq-data-table/miq-table-cell.jsx @@ -143,7 +143,7 @@ const MiqTableCell = ({ disabled={item.disabled} onKeyPress={(e) => cellButtonEvent(item, e)} tabIndex={0} - size={item.size ? item.size : 'default'} + size={item.size ? item.size : 'sm'} title={item.title ? item.title : truncateText} kind={item.kind ? item.kind : 'primary'} className={classNames('miq-data-table-button', item.buttonClassName)}