Skip to content

Commit

Permalink
Method list dialog conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffibm committed May 7, 2024
1 parent 3e0cd03 commit d52357d
Show file tree
Hide file tree
Showing 9 changed files with 462 additions and 0 deletions.
19 changes: 19 additions & 0 deletions app/controllers/miq_ae_class_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,25 @@ 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("id, name")
render :json => {:domains => domains}
end

def ae_methods
methods = MiqAeMethod
.name_path_search(params[:search])
.domain_search(params[:domain_id])
.selected_methods(params[:ids])
.select("id, relative_path, name")
.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

private

def feature_by_action
Expand Down
56 changes: 56 additions & 0 deletions app/javascript/components/AeInlineMethod/FilterNamespace.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Select, SelectItem, Search,
} from 'carbon-components-react';
import { noSelect } from './helper';

const FilterNamespace = ({ domains, onSearch }) => {
/** Function to render the search text. */
const renderSearchText = () => (
<div className="search-wrapper">
<label className="bx--label" htmlFor="Search">{__('Type to search')}</label>
<Search
id="search-method"
labelText={__('Search')}
placeholder={__('Search with Name or Relative path')}
onClear={() => onSearch({ searchText: noSelect })}
onChange={(event) => onSearch({ searchText: event.target.value || noSelect })}
/>
</div>
);

/** Function to render the domain items in a drop-down list. */
const renderDomainList = () => (
<Select
id="domain_id"
labelText="Select a domain"
defaultValue="option"
size="lg"
onChange={(event) => onSearch({ selectedDomain: event.target.value })}
>
<SelectItem value={noSelect} text="None" />
{
domains.map((domain) => <SelectItem key={domain.id} value={domain.id} text={domain.name} />)
}
</Select>
);

return (
<div className="inline-filters">
{renderSearchText()}
{domains && renderDomainList()}
</div>
);
};

export default FilterNamespace;

FilterNamespace.propTypes = {
domains: PropTypes.arrayOf(PropTypes.any),
onSearch: PropTypes.func.isRequired,
};

FilterNamespace.defaultProps = {
domains: undefined,
};
102 changes: 102 additions & 0 deletions app/javascript/components/AeInlineMethod/NamespaceSelector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
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, selectedIds }) => {
const [data, setData] = useState({
domains: [],
methods: [],
loading: true,
searchText: undefined,
selectedDomain: undefined,
});

/** Function to update the component state data. */
const updateData = (updatedData) => setData({ ...data, ...updatedData });

/** Loads the 'domains' and 'methods' from its respective URL's during the component's onLoad event. */
useEffect(() => {
Promise.all([
http.get(namespaceUrls.aeDomainsUrl),
http.get(namespaceUrls.aeMethodsUrl)])
.then(([{ domains }, { methods }]) => updateData({ loading: false, domains, methods: formatMethods(methods) }));
}, []);

const retrieveMethods = async(url) => {
const response = await http.get(url);
return response.methods;
};

/** Function to handle search text and drop-down item onchange events. */
const onSearch = async(filterData) => {
updateData({ loading: true });
const searchText = filterData.searchText ? filterData.searchText : data.searchText;
const selectedDomain = filterData.selectedDomain ? filterData.selectedDomain : data.selectedDomain;
const url = searchUrl(selectedDomain, searchText);
try {
const methods = await retrieveMethods(url);
updateData({
loading: false,
selectedDomain,
searchText,
methods: formatMethods(methods),
});
} catch (error) {
console.error('Error retrieving methods:', error);
updateData({ loading: false }); // Update loading state even if there's an error
}
};

/** Function to handle the click events for the list. */
const onCellClick = (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
? (
<MiqDataTable
headers={methodSelectorHeaders}
stickyHeader
rows={data.methods}
mode="miq-inline-method-list"
rowCheckBox
sortable={false}
gridChecks={selectedIds}
onCellClick={(selectedRow, cellType, event) => onCellClick(selectedRow, cellType, event.target.checked)}
/>
)
: <NotificationMessage type="error" message={__('No methods available.')} />);

return (
<div className="inline-method-selector">
<FilterNamespace domains={data.domains} onSearch={(filterData) => onSearch(filterData)} />
<div className="inline-contents-wrapper">
{
data.loading
? <Loading active small withOverlay={false} className="loading" />
: renderContents()
}
</div>
</div>
);
};

export default NamespaceSelector;

NamespaceSelector.propTypes = {
onSelectMethod: PropTypes.func.isRequired,
selectedIds: PropTypes.arrayOf(PropTypes.any).isRequired,
};
59 changes: 59 additions & 0 deletions app/javascript/components/AeInlineMethod/helper.js
Original file line number Diff line number Diff line change
@@ -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}`;
};
146 changes: 146 additions & 0 deletions app/javascript/components/AeInlineMethod/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import {
Modal, Button, ModalBody, Accordion, AccordionItem,
} from 'carbon-components-react';
import { AddAlt16 } from '@carbon/icons-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,
selectedIds: [],
rows: [],
});

/** Function to show/hide the modal. */
const showModal = (status) => setData({ ...data, isModalOpen: status });

/** Function to handle the select-all check-box click event. */
const onSelectAll = (selectedItems, checked) => setData({ ...data, selectedIds: checked ? [...selectedItems] : [] });

/** Function to handle the list row selection events.
* selectedItem is passed as an array. */
const onItemSelect = (selectedItems, checked) => {
if (checked) {
data.selectedIds.push(selectedItems[0].id);
} else {
data.selectedIds = data.selectedIds.filter((item) => item !== selectedItems[0].id);
}
setData({ ...data, selectedIds: [...data.selectedIds] });
};

/** 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 && item.callbackAction === 'removeMethod') {
setData({
rows: data.rows.filter((row) => row.id !== item.id),
selectedIds: data.selectedIds.filter((id) => id !== item.id),
});
}
};

/** Function to handle the modal submit action. */
const submitModal = () => {
if (data.selectedIds.length > 0) {
http.get(`${namespaceUrls.aeMethodsUrl}?ids=${data.selectedIds.map((str) => parseInt(str, 10))}`)
.then(({ methods }) => setData({ ...data, rows: formatListMethods(methods), isModalOpen: false }));
} else {
setData({ ...data, rows: [], isModalOpen: false });
}
};

/** Function to render the modal with namespace selector component. */
const renderModalSelector = () => (
<Modal
size="lg"
modalHeading={__('Select item')}
className="ae-inline-method-modal"
open={data.isModalOpen}
primaryButtonText={__('OK')}
secondaryButtonText={__('Cancel')}
onRequestClose={() => showModal(false)}
onRequestSubmit={() => submitModal()}
onSecondarySubmit={() => showModal(false)}
>
<ModalBody>
{
data.isModalOpen
&& (
<NamespaceSelector
onSelectMethod={({ selectedItems, cellType, checked }) => onSelectMethod(selectedItems, cellType, checked)}
selectedIds={data.selectedIds}
/>
)
}
</ModalBody>
</Modal>
);

/** Function to render the contents of the list. */
const renderList = () => (data.rows && data.rows.length > 0
? (
<MiqDataTable
headers={methodListHeaders}
rows={data.rows}
mode="miq-inline-method-list"
sortable={false}
onCellClick={(selectedRow) => onCellClickHandler(selectedRow)}
/>
)
: (
<div className="ae-inline-methods-notification">
<NotificationMessage type="info" message={__('No methods selected.')} />
</div>
));

const renderAddButton = () => (
<div className="custom-form-buttons">
<Button
id="add-method"
kind="primary"
title={__('Add Method')}
renderIcon={AddAlt16}
onClick={() => showModal(true)}
size="sm"
>
{__('Add method')}
</Button>
</div>
);

const renderCustomContents = () => (
<Accordion align="start" className="miq-custom-form-accordion">
<AccordionItem title={__('Methods')} open>
{renderAddButton()}
{renderList()}
</AccordionItem>
</Accordion>
);

return (
<div className="custom-form-wrapper">
{renderCustomContents()}
{renderModalSelector()}
</div>
);
};

export default AeInlineMethod;

AeInlineMethod.propTypes = {
type: PropTypes.string.isRequired,
};
Loading

0 comments on commit d52357d

Please sign in to comment.