Skip to content

Commit 98671ab

Browse files
jeffibmkbrock
authored andcommitted
Method list dialog conversion
1 parent f9d74a6 commit 98671ab

File tree

15 files changed

+651
-45
lines changed

15 files changed

+651
-45
lines changed

app/controllers/miq_ae_class_controller.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@ def edit_method
520520
id = x_node.split('-')
521521
end
522522
@ae_method = find_record_with_rbac(MiqAeMethod, id[1])
523+
@embedded_methods = MiqAeMethod.where(:relative_path => @ae_method[:embedded_methods].map { |str| str.sub(/^\//, '') })
523524
@selectable_methods = embedded_method_regex(@ae_method.fqname)
524525
if playbook_style_location?(@ae_method.location)
525526
# these variants are implemented in Angular
@@ -1815,6 +1816,32 @@ def namespace
18151816
render :json => find_record_with_rbac(MiqAeNamespace, params[:id]).attributes.slice('name', 'description', 'enabled')
18161817
end
18171818

1819+
def ae_domains
1820+
domains = MiqAeDomain.where(:enabled => true).order("name").select("id, name")
1821+
render :json => {:domains => domains}
1822+
end
1823+
1824+
def ae_methods
1825+
methods = MiqAeMethod
1826+
.name_path_search(params[:search])
1827+
.where(params[:domain_id] ? {:domain_id => params[:domain_id]} : {})
1828+
.where(params[:ids] ? {:id => params[:ids]&.split(',')} : {})
1829+
.select("id, relative_path, name")
1830+
.order('name')
1831+
render :json => {:methods => methods}
1832+
end
1833+
1834+
def ae_method_operations
1835+
ids = params[:ids].split(",")
1836+
@edit[:new][:embedded_methods] = MiqAeMethod.where(:id => ids).pluck(:relative_path).map { |path| "/#{path}" }
1837+
@changed = true
1838+
render :update do |page|
1839+
page << javascript_prologue
1840+
page << javascript_for_miq_button_visibility(@changed)
1841+
page << "miqSparkle(false);"
1842+
end
1843+
end
1844+
18181845
private
18191846

18201847
def feature_by_action
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import {
4+
Select, SelectItem, Search,
5+
} from 'carbon-components-react';
6+
import { noSelect } from './helper';
7+
8+
const FilterNamespace = ({ domains, onSearch }) => {
9+
/** Function to render the search text. */
10+
const renderSearchText = () => (
11+
<div className="search-wrapper">
12+
<label className="bx--label" htmlFor="Search">{__('Type to search')}</label>
13+
<Search
14+
id="search-method"
15+
labelText={__('Search')}
16+
placeholder={__('Search with Name or Relative path')}
17+
onClear={() => onSearch({ searchText: noSelect })}
18+
onChange={(event) => onSearch({ searchText: event.target.value || noSelect })}
19+
/>
20+
</div>
21+
);
22+
23+
/** Function to render the domain items in a drop-down list. */
24+
const renderDomainList = () => (
25+
<Select
26+
id="domain_id"
27+
labelText="Select a domain"
28+
defaultValue="option"
29+
size="lg"
30+
onChange={(event) => onSearch({ selectedDomain: event.target.value })}
31+
>
32+
<SelectItem value={noSelect} text="None" />
33+
{
34+
domains.map((domain) => <SelectItem key={domain.id} value={domain.id} text={domain.name} />)
35+
}
36+
</Select>
37+
);
38+
39+
return (
40+
<div className="inline-filters">
41+
{renderSearchText()}
42+
{domains && renderDomainList()}
43+
</div>
44+
);
45+
};
46+
47+
export default FilterNamespace;
48+
49+
FilterNamespace.propTypes = {
50+
domains: PropTypes.arrayOf(PropTypes.any),
51+
onSearch: PropTypes.func.isRequired,
52+
};
53+
54+
FilterNamespace.defaultProps = {
55+
domains: undefined,
56+
};
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import React, { useState, useMemo, useCallback } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { useQuery } from 'react-query';
4+
import { Loading } from 'carbon-components-react';
5+
import { debounce } from 'lodash';
6+
import FilterNamespace from './FilterNamespace';
7+
import MiqDataTable from '../miq-data-table';
8+
import NotificationMessage from '../notification-message';
9+
import { CellAction } from '../miq-data-table/helper';
10+
import {
11+
methodSelectorHeaders, formatMethods, searchUrl, namespaceUrls,
12+
} from './helper';
13+
import './style.scss';
14+
15+
const NamespaceSelector = ({ onSelectMethod, selectedIds }) => {
16+
const [filterData, setFilterData] = useState({ searchText: '', selectedDomain: '' });
17+
18+
/** Loads the domains and stores in domainData for 60 seconds. */
19+
const { data: domainsData, isLoading: domainsLoading } = useQuery(
20+
'domainsData',
21+
async() => (await http.get(namespaceUrls.aeDomainsUrl)).domains,
22+
{
23+
staleTime: 60000,
24+
}
25+
);
26+
27+
/** Loads the methods and stores in methodsData for 60 seconds.
28+
* If condition works on page load
29+
* Else part would work if there is a change in filterData.
30+
*/
31+
const { data, isLoading: methodsLoading } = useQuery(
32+
['methodsData', filterData.searchText, filterData.selectedDomain],
33+
async() => {
34+
if (!filterData.searchText && !filterData.selectedDomain) {
35+
const response = await http.get(namespaceUrls.aeMethodsUrl);
36+
return formatMethods(response.methods);
37+
}
38+
const url = searchUrl(filterData.selectedDomain, filterData.searchText);
39+
const response = await http.get(url);
40+
return formatMethods(response.methods);
41+
},
42+
{
43+
keepPreviousData: true,
44+
refetchOnWindowFocus: false,
45+
staleTime: 60000,
46+
}
47+
);
48+
49+
/** Debounce the search text by delaying the text input provided to the API. */
50+
const debouncedSearch = debounce((newFilterData) => {
51+
setFilterData(newFilterData);
52+
}, 300);
53+
54+
/** Function to handle the onSearch event during a filter change event. */
55+
const onSearch = useCallback(
56+
(newFilterData) => debouncedSearch(newFilterData),
57+
[debouncedSearch]
58+
);
59+
60+
/** Function to handle the click event of a cell in the data table. */
61+
const onCellClick = (selectedRow, cellType, checked) => {
62+
const selectedItems = cellType === CellAction.selectAll
63+
? data && data.map((item) => item.id)
64+
: [selectedRow];
65+
onSelectMethod({ selectedItems, cellType, checked });
66+
};
67+
68+
/** Function to render the list which depends on the data and selectedIds.
69+
* List is memoized to prevent unnecessary re-renders when other state values change. */
70+
const renderContents = useMemo(() => {
71+
if (!data || data.length === 0) {
72+
return <NotificationMessage type="info" message={__('No methods available.')} />;
73+
}
74+
75+
return (
76+
<MiqDataTable
77+
headers={methodSelectorHeaders}
78+
stickyHeader
79+
rows={data}
80+
mode="miq-inline-method-list"
81+
rowCheckBox
82+
sortable={false}
83+
gridChecks={selectedIds}
84+
onCellClick={(selectedRow, cellType, event) => onCellClick(selectedRow, cellType, event.target.checked)}
85+
/>
86+
);
87+
}, [data, selectedIds]);
88+
89+
return (
90+
<div className="inline-method-selector">
91+
<FilterNamespace domains={domainsData} onSearch={onSearch} />
92+
<div className="inline-contents-wrapper">
93+
{(domainsLoading || methodsLoading)
94+
? <Loading active small withOverlay={false} className="loading" />
95+
: renderContents}
96+
</div>
97+
</div>
98+
);
99+
};
100+
101+
NamespaceSelector.propTypes = {
102+
onSelectMethod: PropTypes.func.isRequired,
103+
selectedIds: PropTypes.arrayOf(PropTypes.any).isRequired,
104+
};
105+
106+
export default NamespaceSelector;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
export const namespaceUrls = {
2+
aeMethodsUrl: '/miq_ae_class/ae_methods',
3+
aeMethodOperationsUrl: '/miq_ae_class/ae_method_operations',
4+
aeDomainsUrl: '/miq_ae_class/ae_domains',
5+
};
6+
7+
export const noSelect = 'NONE';
8+
9+
/** Headers needed for the data-table list. */
10+
export const methodSelectorHeaders = [
11+
{
12+
key: 'name',
13+
header: 'Name',
14+
},
15+
{
16+
key: 'path',
17+
header: 'Relative path',
18+
},
19+
];
20+
21+
export const methodListHeaders = [
22+
...methodSelectorHeaders,
23+
{ key: 'remove', header: __('Remove'), actionCell: true },
24+
];
25+
26+
/** Function to format the method data needed for the data-table list. */
27+
export const formatMethods = (methods) => (methods.map((item) => ({
28+
id: item.id.toString(),
29+
name: { text: item.name, icon: 'icon node-icon fa-ruby' },
30+
path: item.relative_path,
31+
})));
32+
33+
const removeMethodButton = () => ({
34+
is_button: true,
35+
actionCell: true,
36+
title: __('Remove'),
37+
text: __('Remove'),
38+
alt: __('Remove'),
39+
kind: 'danger',
40+
callback: 'removeMethod',
41+
});
42+
43+
export const formatListMethods = (methods) => (methods.map((item, index) => ({
44+
id: item.id.toString(),
45+
name: { text: item.name, icon: 'icon node-icon fa-ruby' },
46+
path: item.relative_path,
47+
remove: removeMethodButton(item, index),
48+
})));
49+
50+
/** Function to return a conditional URL based on the selected filters. */
51+
export const searchUrl = (selectedDomain, text) => {
52+
const queryParams = [];
53+
if (selectedDomain && selectedDomain !== noSelect) {
54+
queryParams.push(`domain_id=${selectedDomain}`);
55+
}
56+
if (text && text !== noSelect) {
57+
queryParams.push(`search=${text}`);
58+
}
59+
const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
60+
return `${namespaceUrls.aeMethodsUrl}${queryString}`;
61+
};

0 commit comments

Comments
 (0)