Skip to content

Commit

Permalink
Method list dialog conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffibm authored and kbrock committed Jul 25, 2024
1 parent f9d74a6 commit 98671ab
Show file tree
Hide file tree
Showing 15 changed files with 651 additions and 45 deletions.
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(/^\//, '') })
@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

0 comments on commit 98671ab

Please sign in to comment.