-
Notifications
You must be signed in to change notification settings - Fork 356
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
468 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 56 additions & 0 deletions
56
app/javascript/components/AeInlineMethod/FilterNamespace.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
101 changes: 101 additions & 0 deletions
101
app/javascript/components/AeInlineMethod/NamespaceSelector.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import React, { useState } 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 = (newFilterData) => debouncedSearch(newFilterData); | ||
|
||
/** 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 }); | ||
}; | ||
|
||
const renderContents = () => { | ||
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)} | ||
/> | ||
); | ||
}; | ||
|
||
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import React, { useState } from 'react'; | ||
import { QueryClient, QueryClientProvider } from 'react-query'; | ||
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 queryClient = new QueryClient(); | ||
|
||
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 | ||
&& ( | ||
<QueryClientProvider client={queryClient}> | ||
<NamespaceSelector | ||
onSelectMethod={({ selectedItems, cellType, checked }) => onSelectMethod(selectedItems, cellType, checked)} | ||
selectedIds={data.selectedIds} | ||
/> | ||
</QueryClientProvider> | ||
) | ||
} | ||
</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, | ||
}; |
Oops, something went wrong.