mirror of
https://github.com/ansible/awx.git
synced 2026-03-21 02:47:35 -02:30
limit advanced search options by field type
This commit is contained in:
@@ -200,7 +200,7 @@ DataListToolbar.propTypes = {
|
|||||||
clearAllFilters: PropTypes.func,
|
clearAllFilters: PropTypes.func,
|
||||||
qsConfig: QSConfig.isRequired,
|
qsConfig: QSConfig.isRequired,
|
||||||
searchColumns: SearchColumns.isRequired,
|
searchColumns: SearchColumns.isRequired,
|
||||||
searchableKeys: PropTypes.arrayOf(PropTypes.string),
|
searchableKeys: PropTypes.arrayOf(PropTypes.string), // TODO: Update
|
||||||
relatedSearchableKeys: PropTypes.arrayOf(PropTypes.string),
|
relatedSearchableKeys: PropTypes.arrayOf(PropTypes.string),
|
||||||
sortColumns: SortColumns,
|
sortColumns: SortColumns,
|
||||||
isAllSelected: PropTypes.bool,
|
isAllSelected: PropTypes.bool,
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { SearchIcon, QuestionCircleIcon } from '@patternfly/react-icons';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useConfig } from 'contexts/Config';
|
import { useConfig } from 'contexts/Config';
|
||||||
import getDocsBaseUrl from 'util/getDocsBaseUrl';
|
import getDocsBaseUrl from 'util/getDocsBaseUrl';
|
||||||
|
import RelatedLookupTypeInput from './RelatedLookupTypeInput';
|
||||||
|
import LookupTypeInput from './LookupTypeInput';
|
||||||
|
|
||||||
const AdvancedGroup = styled.div`
|
const AdvancedGroup = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -42,31 +44,36 @@ function AdvancedSearch({
|
|||||||
// for now, I'm spreading set to get rid of duplicate keys...when they are grouped
|
// for now, I'm spreading set to get rid of duplicate keys...when they are grouped
|
||||||
// we might want to revisit that.
|
// we might want to revisit that.
|
||||||
const allKeys = [
|
const allKeys = [
|
||||||
...new Set([...(searchableKeys || []), ...(relatedSearchableKeys || [])]),
|
...new Set([
|
||||||
|
...(searchableKeys.map((k) => k.key) || []),
|
||||||
|
...(relatedSearchableKeys || []),
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
const [isPrefixDropdownOpen, setIsPrefixDropdownOpen] = useState(false);
|
const [isPrefixDropdownOpen, setIsPrefixDropdownOpen] = useState(false);
|
||||||
const [isLookupDropdownOpen, setIsLookupDropdownOpen] = useState(false);
|
|
||||||
const [isKeyDropdownOpen, setIsKeyDropdownOpen] = useState(false);
|
const [isKeyDropdownOpen, setIsKeyDropdownOpen] = useState(false);
|
||||||
const [prefixSelection, setPrefixSelection] = useState(null);
|
const [prefixSelection, setPrefixSelection] = useState(null);
|
||||||
const [lookupSelection, setLookupSelection] = useState(null);
|
const [lookupSelection, setLookupSelection] = useState(null);
|
||||||
const [keySelection, setKeySelection] = useState(null);
|
const [keySelection, setKeySelection] = useState(null);
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const [relatedSearchKeySelected, setRelatedSearchKeySelected] =
|
// const [relatedSearchKeySelected, setRelatedSearchKeySelected] =
|
||||||
useState(false);
|
// useState(false);
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
|
|
||||||
|
const relatedSearchKeySelected =
|
||||||
|
keySelection &&
|
||||||
|
relatedSearchableKeys.indexOf(keySelection) > -1 &&
|
||||||
|
!searchableKeys.find((k) => k.key === keySelection);
|
||||||
|
const lookupKeyType =
|
||||||
|
keySelection && !relatedSearchKeySelected
|
||||||
|
? searchableKeys.find((k) => k.key === keySelection).type
|
||||||
|
: null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (relatedSearchKeySelected) {
|
||||||
keySelection &&
|
|
||||||
relatedSearchableKeys.indexOf(keySelection) > -1 &&
|
|
||||||
searchableKeys.indexOf(keySelection) === -1
|
|
||||||
) {
|
|
||||||
setLookupSelection('name__icontains');
|
setLookupSelection('name__icontains');
|
||||||
setRelatedSearchKeySelected(true);
|
|
||||||
} else {
|
} else {
|
||||||
setLookupSelection(null);
|
setLookupSelection(null);
|
||||||
setRelatedSearchKeySelected(false);
|
|
||||||
}
|
}
|
||||||
}, [keySelection]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [keySelection]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
@@ -136,160 +143,6 @@ function AdvancedSearch({
|
|||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderRelatedLookupType = () => (
|
|
||||||
<Select
|
|
||||||
ouiaId="set-lookup-typeahead"
|
|
||||||
aria-label={t`Related search type`}
|
|
||||||
className="lookupSelect"
|
|
||||||
variant={SelectVariant.typeahead}
|
|
||||||
typeAheadAriaLabel={t`Related search type typeahead`}
|
|
||||||
onToggle={setIsLookupDropdownOpen}
|
|
||||||
onSelect={(event, selection) => setLookupSelection(selection)}
|
|
||||||
selections={lookupSelection}
|
|
||||||
isOpen={isLookupDropdownOpen}
|
|
||||||
placeholderText={t`Related search type`}
|
|
||||||
maxHeight={maxSelectHeight}
|
|
||||||
noResultsFoundText={t`No results found`}
|
|
||||||
>
|
|
||||||
<SelectOption
|
|
||||||
id="name-option-select"
|
|
||||||
key="name__icontains"
|
|
||||||
value="name__icontains"
|
|
||||||
description={t`Fuzzy search on name field.`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
|
||||||
id="id-option-select"
|
|
||||||
key="id"
|
|
||||||
value="id"
|
|
||||||
description={t`Exact search on id field.`}
|
|
||||||
/>
|
|
||||||
{enableRelatedFuzzyFiltering && (
|
|
||||||
<SelectOption
|
|
||||||
id="search-option-select"
|
|
||||||
key="search"
|
|
||||||
value="search"
|
|
||||||
description={t`Fuzzy search on id, name or description fields.`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderLookupType = () => (
|
|
||||||
<Select
|
|
||||||
ouiaId="set-lookup-typeahead"
|
|
||||||
aria-label={t`Lookup select`}
|
|
||||||
className="lookupSelect"
|
|
||||||
variant={SelectVariant.typeahead}
|
|
||||||
typeAheadAriaLabel={t`Lookup typeahead`}
|
|
||||||
onToggle={setIsLookupDropdownOpen}
|
|
||||||
onSelect={(event, selection) => setLookupSelection(selection)}
|
|
||||||
onClear={() => setLookupSelection(null)}
|
|
||||||
selections={lookupSelection}
|
|
||||||
isOpen={isLookupDropdownOpen}
|
|
||||||
placeholderText={t`Lookup type`}
|
|
||||||
maxHeight={maxSelectHeight}
|
|
||||||
noResultsFoundText={t`No results found`}
|
|
||||||
>
|
|
||||||
<SelectOption
|
|
||||||
id="exact-option-select"
|
|
||||||
key="exact"
|
|
||||||
value="exact"
|
|
||||||
description={t`Exact match (default lookup if not specified).`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
|
||||||
id="iexact-option-select"
|
|
||||||
key="iexact"
|
|
||||||
value="iexact"
|
|
||||||
description={t`Case-insensitive version of exact.`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectOption
|
|
||||||
id="contains-option-select"
|
|
||||||
key="contains"
|
|
||||||
value="contains"
|
|
||||||
description={t`Field contains value.`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
|
||||||
id="icontains-option-select"
|
|
||||||
key="icontains"
|
|
||||||
value="icontains"
|
|
||||||
description={t`Case-insensitive version of contains`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
|
||||||
id="startswith-option-select"
|
|
||||||
key="startswith"
|
|
||||||
value="startswith"
|
|
||||||
description={t`Field starts with value.`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
|
||||||
id="istartswith-option-select"
|
|
||||||
key="istartswith"
|
|
||||||
value="istartswith"
|
|
||||||
description={t`Case-insensitive version of startswith.`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
|
||||||
id="endswith-option-select"
|
|
||||||
key="endswith"
|
|
||||||
value="endswith"
|
|
||||||
description={t`Field ends with value.`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
|
||||||
id="iendswith-option-select"
|
|
||||||
key="iendswith"
|
|
||||||
value="iendswith"
|
|
||||||
description={t`Case-insensitive version of endswith.`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
|
||||||
id="regex-option-select"
|
|
||||||
key="regex"
|
|
||||||
value="regex"
|
|
||||||
description={t`Field matches the given regular expression.`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
|
||||||
id="iregex-option-select"
|
|
||||||
key="iregex"
|
|
||||||
value="iregex"
|
|
||||||
description={t`Case-insensitive version of regex.`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
|
||||||
id="gt-option-select"
|
|
||||||
key="gt"
|
|
||||||
value="gt"
|
|
||||||
description={t`Greater than comparison.`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
|
||||||
id="gte-option-select"
|
|
||||||
key="gte"
|
|
||||||
value="gte"
|
|
||||||
description={t`Greater than or equal to comparison.`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
|
||||||
id="lt-option-select"
|
|
||||||
key="lt"
|
|
||||||
value="lt"
|
|
||||||
description={t`Less than comparison.`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
|
||||||
id="lte-option-select"
|
|
||||||
key="lte"
|
|
||||||
value="lte"
|
|
||||||
description={t`Less than or equal to comparison.`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
|
||||||
id="isnull-option-select"
|
|
||||||
key="isnull"
|
|
||||||
value="isnull"
|
|
||||||
description={t`Check whether the given field or related object is null; expects a boolean value.`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
|
||||||
id="in-option-select"
|
|
||||||
key="in"
|
|
||||||
value="in"
|
|
||||||
description={t`Check whether the given field's value is present in the list provided; expects a comma-separated list of items.`}
|
|
||||||
/>
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdvancedGroup>
|
<AdvancedGroup>
|
||||||
{lookupSelection === 'search' ? (
|
{lookupSelection === 'search' ? (
|
||||||
@@ -328,9 +181,21 @@ function AdvancedSearch({
|
|||||||
</SelectOption>
|
</SelectOption>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
{relatedSearchKeySelected
|
{relatedSearchKeySelected ? (
|
||||||
? renderRelatedLookupType()
|
<RelatedLookupTypeInput
|
||||||
: renderLookupType()}
|
value={lookupSelection}
|
||||||
|
setValue={setLookupSelection}
|
||||||
|
maxSelectHeight={maxSelectHeight}
|
||||||
|
enableFuzzyFiltering={enableRelatedFuzzyFiltering}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LookupTypeInput
|
||||||
|
value={lookupSelection}
|
||||||
|
type={lookupKeyType}
|
||||||
|
setValue={setLookupSelection}
|
||||||
|
maxSelectHeight={maxSelectHeight}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<TextInput
|
<TextInput
|
||||||
data-cy="advanced-search-text-input"
|
data-cy="advanced-search-text-input"
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ function OrganizationsList() {
|
|||||||
OrganizationsAPI.read(params),
|
OrganizationsAPI.read(params),
|
||||||
OrganizationsAPI.readOptions(),
|
OrganizationsAPI.readOptions(),
|
||||||
]);
|
]);
|
||||||
|
const keys = orgActions.data.actions?.GET || {};
|
||||||
return {
|
return {
|
||||||
organizations: orgs.data.results,
|
organizations: orgs.data.results,
|
||||||
organizationCount: orgs.data.count,
|
organizationCount: orgs.data.count,
|
||||||
@@ -56,9 +57,15 @@ function OrganizationsList() {
|
|||||||
relatedSearchableKeys: (
|
relatedSearchableKeys: (
|
||||||
orgActions?.data?.related_search_fields || []
|
orgActions?.data?.related_search_fields || []
|
||||||
).map((val) => val.slice(0, -8)),
|
).map((val) => val.slice(0, -8)),
|
||||||
searchableKeys: Object.keys(orgActions.data.actions?.GET || {}).filter(
|
// searchableKeys: Object.keys(orgActions.data.actions?.GET || {}).filter(
|
||||||
(key) => orgActions.data.actions?.GET[key].filterable
|
// (key) => orgActions.data.actions?.GET[key].filterable
|
||||||
),
|
// ),
|
||||||
|
searchableKeys: Object.keys(keys)
|
||||||
|
.filter((key) => keys[key].filterable)
|
||||||
|
.map((key) => ({
|
||||||
|
key,
|
||||||
|
type: keys[key].type,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
}, [location]),
|
}, [location]),
|
||||||
{
|
{
|
||||||
|
|||||||
141
awx/ui_next/src/components/Search/LookupTypeInput.js
Normal file
141
awx/ui_next/src/components/Search/LookupTypeInput.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
function Option({ show, ...props }) {
|
||||||
|
if (!show) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <SelectOption {...props} />;
|
||||||
|
}
|
||||||
|
Option.defaultProps = {
|
||||||
|
show: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
function LookupTypeInput({ value, type, setValue, maxSelectHeight }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
ouiaId="set-lookup-typeahead"
|
||||||
|
aria-label={t`Lookup select`}
|
||||||
|
className="lookupSelect"
|
||||||
|
variant={SelectVariant.typeahead}
|
||||||
|
typeAheadAriaLabel={t`Lookup typeahead`}
|
||||||
|
onToggle={setIsOpen}
|
||||||
|
onSelect={(event, selection) => setValue(selection)}
|
||||||
|
onClear={() => setValue(null)}
|
||||||
|
selections={value}
|
||||||
|
isOpen={isOpen}
|
||||||
|
placeholderText={t`Lookup type`}
|
||||||
|
maxHeight={maxSelectHeight}
|
||||||
|
noResultsFoundText={t`No results found`}
|
||||||
|
>
|
||||||
|
<Option
|
||||||
|
id="exact-option-select"
|
||||||
|
key="exact"
|
||||||
|
value="exact"
|
||||||
|
description={t`Exact match (default lookup if not specified).`}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
id="iexact-option-select"
|
||||||
|
key="iexact"
|
||||||
|
value="iexact"
|
||||||
|
description={t`Case-insensitive version of exact.`}
|
||||||
|
show={type === 'string'}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
id="contains-option-select"
|
||||||
|
key="contains"
|
||||||
|
value="contains"
|
||||||
|
description={t`Field contains value.`}
|
||||||
|
show={type === 'string'}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
id="icontains-option-select"
|
||||||
|
key="icontains"
|
||||||
|
value="icontains"
|
||||||
|
description={t`Case-insensitive version of contains`}
|
||||||
|
show={type === 'string'}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
id="startswith-option-select"
|
||||||
|
key="startswith"
|
||||||
|
value="startswith"
|
||||||
|
description={t`Field starts with value.`}
|
||||||
|
show={type !== 'datetime'}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
id="istartswith-option-select"
|
||||||
|
key="istartswith"
|
||||||
|
value="istartswith"
|
||||||
|
description={t`Case-insensitive version of startswith.`}
|
||||||
|
show={type !== 'datetime'}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
id="endswith-option-select"
|
||||||
|
key="endswith"
|
||||||
|
value="endswith"
|
||||||
|
description={t`Field ends with value.`}
|
||||||
|
show={type !== 'datetime'}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
id="iendswith-option-select"
|
||||||
|
key="iendswith"
|
||||||
|
value="iendswith"
|
||||||
|
description={t`Case-insensitive version of endswith.`}
|
||||||
|
show={type !== 'datetime'}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
id="regex-option-select"
|
||||||
|
key="regex"
|
||||||
|
value="regex"
|
||||||
|
description={t`Field matches the given regular expression.`}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
id="iregex-option-select"
|
||||||
|
key="iregex"
|
||||||
|
value="iregex"
|
||||||
|
description={t`Case-insensitive version of regex.`}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
id="gt-option-select"
|
||||||
|
key="gt"
|
||||||
|
value="gt"
|
||||||
|
description={t`Greater than comparison.`}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
id="gte-option-select"
|
||||||
|
key="gte"
|
||||||
|
value="gte"
|
||||||
|
description={t`Greater than or equal to comparison.`}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
id="lt-option-select"
|
||||||
|
key="lt"
|
||||||
|
value="lt"
|
||||||
|
description={t`Less than comparison.`}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
id="lte-option-select"
|
||||||
|
key="lte"
|
||||||
|
value="lte"
|
||||||
|
description={t`Less than or equal to comparison.`}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
id="isnull-option-select"
|
||||||
|
key="isnull"
|
||||||
|
value="isnull"
|
||||||
|
description={t`Check whether the given field or related object is null; expects a boolean value.`}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
id="in-option-select"
|
||||||
|
key="in"
|
||||||
|
value="in"
|
||||||
|
description={t`Check whether the given field's value is present in the list provided; expects a comma-separated list of items.`}
|
||||||
|
/>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LookupTypeInput;
|
||||||
52
awx/ui_next/src/components/Search/RelatedLookupTypeInput.js
Normal file
52
awx/ui_next/src/components/Search/RelatedLookupTypeInput.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
function RelatedLookupTypeInput({
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
maxSelectHeight,
|
||||||
|
enableFuzzyFiltering,
|
||||||
|
}) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
ouiaId="set-lookup-typeahead"
|
||||||
|
aria-label={t`Related search type`}
|
||||||
|
className="lookupSelect"
|
||||||
|
variant={SelectVariant.typeahead}
|
||||||
|
typeAheadAriaLabel={t`Related search type typeahead`}
|
||||||
|
onToggle={setIsOpen}
|
||||||
|
onSelect={(event, selection) => setValue(selection)}
|
||||||
|
selections={value}
|
||||||
|
isOpen={isOpen}
|
||||||
|
placeholderText={t`Related search type`}
|
||||||
|
maxHeight={maxSelectHeight}
|
||||||
|
noResultsFoundText={t`No results found`}
|
||||||
|
>
|
||||||
|
<SelectOption
|
||||||
|
id="name-option-select"
|
||||||
|
key="name__icontains"
|
||||||
|
value="name__icontains"
|
||||||
|
description={t`Fuzzy search on name field.`}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
id="id-option-select"
|
||||||
|
key="id"
|
||||||
|
value="id"
|
||||||
|
description={t`Exact search on id field.`}
|
||||||
|
/>
|
||||||
|
{enableFuzzyFiltering && (
|
||||||
|
<SelectOption
|
||||||
|
id="search-option-select"
|
||||||
|
key="search"
|
||||||
|
value="search"
|
||||||
|
description={t`Fuzzy search on id, name or description fields.`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RelatedLookupTypeInput;
|
||||||
Reference in New Issue
Block a user