Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filtering and selecting embedded methods used in the inline-methods form pages #9059

Merged
merged 1 commit into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions app/controllers/miq_ae_class_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ def edit_method
id = x_node.split('-')
end
@ae_method = find_record_with_rbac(MiqAeMethod, id[1])
@embedded_methods = MiqAeMethod.where(:relative_path => @ae_method[:embedded_methods].map { |str| str.sub(/^\//, '') })
GilbertCherrie marked this conversation as resolved.
Show resolved Hide resolved
@selectable_methods = embedded_method_regex(@ae_method.fqname)
if playbook_style_location?(@ae_method.location)
# these variants are implemented in Angular
Expand Down Expand Up @@ -1815,6 +1816,32 @@ def namespace
render :json => find_record_with_rbac(MiqAeNamespace, params[:id]).attributes.slice('name', 'description', 'enabled')
end

def ae_domains
domains = MiqAeDomain.where(:enabled => true).order("name").select("id, name")
render :json => {:domains => domains}
end

def ae_methods
methods = MiqAeMethod
.name_path_search(params[:search])
.where(params[:domain_id] ? {:domain_id => params[:domain_id]} : {})
.where(params[:ids] ? {:id => params[:ids]&.split(',')} : {})
.select("id, relative_path, name")
.order('name')
render :json => {:methods => methods}
end

def ae_method_operations
ids = params[:ids].split(",")
@edit[:new][:embedded_methods] = MiqAeMethod.where(:id => ids).pluck(:relative_path).map { |path| "/#{path}" }
@changed = true
render :update do |page|
page << javascript_prologue
page << javascript_for_miq_button_visibility(@changed)
page << "miqSparkle(false);"
end
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,
};
106 changes: 106 additions & 0 deletions app/javascript/components/AeInlineMethod/NamespaceSelector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { useState, useMemo, useCallback } from 'react';
import PropTypes from 'prop-types';
import { useQuery } from 'react-query';
import { Loading } from 'carbon-components-react';
import { debounce } from 'lodash';
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';

const NamespaceSelector = ({ onSelectMethod, selectedIds }) => {
const [filterData, setFilterData] = useState({ searchText: '', selectedDomain: '' });

/** Loads the domains and stores in domainData for 60 seconds. */
const { data: domainsData, isLoading: domainsLoading } = useQuery(
'domainsData',
async() => (await http.get(namespaceUrls.aeDomainsUrl)).domains,
{
staleTime: 60000,
}
);

/** Loads the methods and stores in methodsData for 60 seconds.
* If condition works on page load
* Else part would work if there is a change in filterData.
*/
const { data, isLoading: methodsLoading } = useQuery(
['methodsData', filterData.searchText, filterData.selectedDomain],
async() => {
if (!filterData.searchText && !filterData.selectedDomain) {
const response = await http.get(namespaceUrls.aeMethodsUrl);
return formatMethods(response.methods);
}
const url = searchUrl(filterData.selectedDomain, filterData.searchText);
const response = await http.get(url);
return formatMethods(response.methods);
},
{
keepPreviousData: true,
refetchOnWindowFocus: false,
staleTime: 60000,
}
);

/** Debounce the search text by delaying the text input provided to the API. */
const debouncedSearch = debounce((newFilterData) => {
setFilterData(newFilterData);
}, 300);

/** Function to handle the onSearch event during a filter change event. */
const onSearch = useCallback(
(newFilterData) => debouncedSearch(newFilterData),
[debouncedSearch]
);

/** Function to handle the click event of a cell in the data table. */
const onCellClick = (selectedRow, cellType, checked) => {
const selectedItems = cellType === CellAction.selectAll
? data && data.map((item) => item.id)
: [selectedRow];
onSelectMethod({ selectedItems, cellType, checked });
};

/** Function to render the list which depends on the data and selectedIds.
* List is memoized to prevent unnecessary re-renders when other state values change. */
const renderContents = useMemo(() => {
if (!data || data.length === 0) {
return <NotificationMessage type="info" message={__('No methods available.')} />;
}

return (
<MiqDataTable
headers={methodSelectorHeaders}
stickyHeader
rows={data}
mode="miq-inline-method-list"
rowCheckBox
sortable={false}
gridChecks={selectedIds}
onCellClick={(selectedRow, cellType, event) => onCellClick(selectedRow, cellType, event.target.checked)}
/>
);
}, [data, selectedIds]);

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

NamespaceSelector.propTypes = {
onSelectMethod: PropTypes.func.isRequired,
selectedIds: PropTypes.arrayOf(PropTypes.any).isRequired,
};

export default NamespaceSelector;
61 changes: 61 additions & 0 deletions app/javascript/components/AeInlineMethod/helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export const namespaceUrls = {
aeMethodsUrl: '/miq_ae_class/ae_methods',
aeMethodOperationsUrl: '/miq_ae_class/ae_method_operations',
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: 'remove', header: __('Remove'), actionCell: true },
];

/** 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 removeMethodButton = () => ({
is_button: true,
actionCell: true,
title: __('Remove'),
text: __('Remove'),
alt: __('Remove'),
kind: 'danger',
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,
remove: removeMethodButton(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}`;
};
Loading