mirror of
https://github.com/ansible/awx.git
synced 2026-03-19 18:07:33 -02:30
Merge pull request #7739 from jlmitch5/advSearch
Advanced search Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -11,6 +11,10 @@ class Applications extends Base {
|
|||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readTokenOptions(appId) {
|
||||||
|
return this.http.options(`${this.baseUrl}${appId}/tokens/`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Applications;
|
export default Applications;
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ class Users extends Base {
|
|||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readTokenOptions(userId) {
|
||||||
|
return this.http.options(`${this.baseUrl}${userId}/tokens/`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Users;
|
export default Users;
|
||||||
|
|||||||
@@ -156,16 +156,16 @@ class AddResourceRole extends React.Component {
|
|||||||
const userSearchColumns = [
|
const userSearchColumns = [
|
||||||
{
|
{
|
||||||
name: i18n._(t`Username`),
|
name: i18n._(t`Username`),
|
||||||
key: 'username',
|
key: 'username__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`First Name`),
|
name: i18n._(t`First Name`),
|
||||||
key: 'first_name',
|
key: 'first_name__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Last Name`),
|
name: i18n._(t`Last Name`),
|
||||||
key: 'last_name',
|
key: 'last_name__icontains',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ describe('<SelectResourceStep />', () => {
|
|||||||
const searchColumns = [
|
const searchColumns = [
|
||||||
{
|
{
|
||||||
name: 'Username',
|
name: 'Username',
|
||||||
key: 'username',
|
key: 'username__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -114,16 +114,16 @@ function AssociateModal({
|
|||||||
searchColumns={[
|
searchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
sortColumns={[
|
sortColumns={[
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ class DataListToolbar extends React.Component {
|
|||||||
itemCount,
|
itemCount,
|
||||||
clearAllFilters,
|
clearAllFilters,
|
||||||
searchColumns,
|
searchColumns,
|
||||||
|
searchableKeys,
|
||||||
|
relatedSearchableKeys,
|
||||||
sortColumns,
|
sortColumns,
|
||||||
showSelectAll,
|
showSelectAll,
|
||||||
isAllSelected,
|
isAllSelected,
|
||||||
@@ -64,7 +66,12 @@ class DataListToolbar extends React.Component {
|
|||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Search
|
<Search
|
||||||
qsConfig={qsConfig}
|
qsConfig={qsConfig}
|
||||||
columns={searchColumns}
|
columns={[
|
||||||
|
...searchColumns,
|
||||||
|
{ name: i18n._(t`Advanced`), key: 'advanced' },
|
||||||
|
]}
|
||||||
|
searchableKeys={searchableKeys}
|
||||||
|
relatedSearchableKeys={relatedSearchableKeys}
|
||||||
onSearch={onSearch}
|
onSearch={onSearch}
|
||||||
onReplaceSearch={onReplaceSearch}
|
onReplaceSearch={onReplaceSearch}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
@@ -106,6 +113,8 @@ 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),
|
||||||
|
relatedSearchableKeys: PropTypes.arrayOf(PropTypes.string),
|
||||||
sortColumns: SortColumns.isRequired,
|
sortColumns: SortColumns.isRequired,
|
||||||
showSelectAll: PropTypes.bool,
|
showSelectAll: PropTypes.bool,
|
||||||
isAllSelected: PropTypes.bool,
|
isAllSelected: PropTypes.bool,
|
||||||
@@ -121,6 +130,8 @@ DataListToolbar.propTypes = {
|
|||||||
|
|
||||||
DataListToolbar.defaultProps = {
|
DataListToolbar.defaultProps = {
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
|
searchableKeys: [],
|
||||||
|
relatedSearchableKeys: [],
|
||||||
clearAllFilters: null,
|
clearAllFilters: null,
|
||||||
showSelectAll: false,
|
showSelectAll: false,
|
||||||
isAllSelected: false,
|
isAllSelected: false,
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ describe('<DataListToolbar />', () => {
|
|||||||
const onSelectAll = jest.fn();
|
const onSelectAll = jest.fn();
|
||||||
|
|
||||||
test('it triggers the expected callbacks', () => {
|
test('it triggers the expected callbacks', () => {
|
||||||
const searchColumns = [{ name: 'Name', key: 'name', isDefault: true }];
|
const searchColumns = [
|
||||||
|
{ name: 'Name', key: 'name__icontains', isDefault: true },
|
||||||
|
];
|
||||||
const sortColumns = [{ name: 'Name', key: 'name' }];
|
const sortColumns = [{ name: 'Name', key: 'name' }];
|
||||||
const search = 'button[aria-label="Search submit button"]';
|
const search = 'button[aria-label="Search submit button"]';
|
||||||
const searchTextInput = 'input[aria-label="Search text input"]';
|
const searchTextInput = 'input[aria-label="Search text input"]';
|
||||||
@@ -66,11 +68,12 @@ describe('<DataListToolbar />', () => {
|
|||||||
|
|
||||||
test('dropdown items sortable/searchable columns work', () => {
|
test('dropdown items sortable/searchable columns work', () => {
|
||||||
const sortDropdownToggleSelector = 'button[id="awx-sort"]';
|
const sortDropdownToggleSelector = 'button[id="awx-sort"]';
|
||||||
const searchDropdownToggleSelector = 'button[id="awx-search"]';
|
const searchDropdownToggleSelector =
|
||||||
|
'Select[aria-label="Simple key select"] SelectToggle';
|
||||||
const sortDropdownMenuItems =
|
const sortDropdownMenuItems =
|
||||||
'DropdownMenu > ul[aria-labelledby="awx-sort"]';
|
'DropdownMenu > ul[aria-labelledby="awx-sort"]';
|
||||||
const searchDropdownMenuItems =
|
const searchDropdownMenuItems =
|
||||||
'DropdownMenu > ul[aria-labelledby="awx-search"]';
|
'Select[aria-label="Simple key select"] SelectOption';
|
||||||
|
|
||||||
const NEW_QS_CONFIG = {
|
const NEW_QS_CONFIG = {
|
||||||
namespace: 'organization',
|
namespace: 'organization',
|
||||||
@@ -108,7 +111,7 @@ describe('<DataListToolbar />', () => {
|
|||||||
searchDropdownToggle.simulate('click');
|
searchDropdownToggle.simulate('click');
|
||||||
toolbar.update();
|
toolbar.update();
|
||||||
let searchDropdownItems = toolbar.find(searchDropdownMenuItems).children();
|
let searchDropdownItems = toolbar.find(searchDropdownMenuItems).children();
|
||||||
expect(searchDropdownItems.length).toBe(1);
|
expect(searchDropdownItems.length).toBe(2);
|
||||||
const mockedSortEvent = { target: { innerText: 'Bar' } };
|
const mockedSortEvent = { target: { innerText: 'Bar' } };
|
||||||
searchDropdownItems.at(0).simulate('click', mockedSortEvent);
|
searchDropdownItems.at(0).simulate('click', mockedSortEvent);
|
||||||
toolbar = mountWithContexts(
|
toolbar = mountWithContexts(
|
||||||
@@ -144,7 +147,7 @@ describe('<DataListToolbar />', () => {
|
|||||||
toolbar.update();
|
toolbar.update();
|
||||||
|
|
||||||
searchDropdownItems = toolbar.find(searchDropdownMenuItems).children();
|
searchDropdownItems = toolbar.find(searchDropdownMenuItems).children();
|
||||||
expect(searchDropdownItems.length).toBe(1);
|
expect(searchDropdownItems.length).toBe(2);
|
||||||
|
|
||||||
const mockedSearchEvent = { target: { innerText: 'Bar' } };
|
const mockedSearchEvent = { target: { innerText: 'Bar' } };
|
||||||
searchDropdownItems.at(0).simulate('click', mockedSearchEvent);
|
searchDropdownItems.at(0).simulate('click', mockedSearchEvent);
|
||||||
@@ -283,4 +286,31 @@ describe('<DataListToolbar />', () => {
|
|||||||
const checkbox = toolbar.find('Checkbox');
|
const checkbox = toolbar.find('Checkbox');
|
||||||
expect(checkbox.prop('isChecked')).toBe(true);
|
expect(checkbox.prop('isChecked')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('always adds advanced item to search column array', () => {
|
||||||
|
const searchColumns = [{ name: 'Name', key: 'name', isDefault: true }];
|
||||||
|
const sortColumns = [{ name: 'Name', key: 'name' }];
|
||||||
|
|
||||||
|
toolbar = mountWithContexts(
|
||||||
|
<DataListToolbar
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
searchColumns={searchColumns}
|
||||||
|
sortColumns={sortColumns}
|
||||||
|
onSearch={onSearch}
|
||||||
|
onReplaceSearch={onReplaceSearch}
|
||||||
|
onSort={onSort}
|
||||||
|
onSelectAll={onSelectAll}
|
||||||
|
additionalControls={[
|
||||||
|
<button key="1" id="test" type="button">
|
||||||
|
click
|
||||||
|
</button>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const search = toolbar.find('Search');
|
||||||
|
expect(
|
||||||
|
search.prop('columns').filter(col => col.key === 'advanced').length
|
||||||
|
).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const {
|
const {
|
||||||
result: { results, count },
|
result: { results, count, actions, relatedSearchFields },
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchJobs,
|
request: fetchJobs,
|
||||||
@@ -46,12 +46,27 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
useCallback(
|
useCallback(
|
||||||
async () => {
|
async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
const { data } = await UnifiedJobsAPI.read({ ...params });
|
const [response, actionsResponse] = await Promise.all([
|
||||||
return data;
|
UnifiedJobsAPI.read({ ...params }),
|
||||||
|
UnifiedJobsAPI.readOptions(),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
results: response.data.results,
|
||||||
|
count: response.data.count,
|
||||||
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
[location] // eslint-disable-line react-hooks/exhaustive-deps
|
[location] // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
),
|
),
|
||||||
{ results: [], count: 0 }
|
{
|
||||||
|
results: [],
|
||||||
|
count: 0,
|
||||||
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
|
}
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchJobs();
|
fetchJobs();
|
||||||
@@ -123,6 +138,11 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -137,7 +157,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -146,11 +166,11 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Label Name`),
|
name: i18n._(t`Label Name`),
|
||||||
key: 'labels__name',
|
key: 'labels__name__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Job Type`),
|
name: i18n._(t`Job Type`),
|
||||||
key: `type`,
|
key: `or__type`,
|
||||||
options: [
|
options: [
|
||||||
[`project_update`, i18n._(t`Source Control Update`)],
|
[`project_update`, i18n._(t`Source Control Update`)],
|
||||||
[`inventory_update`, i18n._(t`Inventory Sync`)],
|
[`inventory_update`, i18n._(t`Inventory Sync`)],
|
||||||
@@ -162,7 +182,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Launched By (Username)`),
|
name: i18n._(t`Launched By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Status`),
|
name: i18n._(t`Status`),
|
||||||
@@ -209,6 +229,8 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
key: 'started',
|
key: 'started',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderToolbar={props => (
|
renderToolbar={props => (
|
||||||
<DatalistToolbar
|
<DatalistToolbar
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -96,6 +96,16 @@ UnifiedJobsAPI.read.mockResolvedValue({
|
|||||||
data: { count: 3, results: mockResults },
|
data: { count: 3, results: mockResults },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
UnifiedJobsAPI.readOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {},
|
||||||
|
POST: {},
|
||||||
|
},
|
||||||
|
related_search_fields: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function waitForLoaded(wrapper) {
|
function waitForLoaded(wrapper) {
|
||||||
return waitForElement(
|
return waitForElement(
|
||||||
wrapper,
|
wrapper,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ function CredentialsStep({ i18n }) {
|
|||||||
}, [fetchTypes]);
|
}, [fetchTypes]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { credentials, count },
|
result: { credentials, count, actions, relatedSearchFields },
|
||||||
error: credentialsError,
|
error: credentialsError,
|
||||||
isLoading: isCredentialsLoading,
|
isLoading: isCredentialsLoading,
|
||||||
request: fetchCredentials,
|
request: fetchCredentials,
|
||||||
@@ -62,16 +62,23 @@ function CredentialsStep({ i18n }) {
|
|||||||
return { credentials: [], count: 0 };
|
return { credentials: [], count: 0 };
|
||||||
}
|
}
|
||||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||||
const { data } = await CredentialsAPI.read({
|
const [{ data }, actionsResponse] = await Promise.all([
|
||||||
...params,
|
CredentialsAPI.read({
|
||||||
credential_type: selectedType.id,
|
...params,
|
||||||
});
|
credential_type: selectedType.id,
|
||||||
|
}),
|
||||||
|
CredentialsAPI.readOptions(),
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
credentials: data.results,
|
credentials: data.results,
|
||||||
count: data.count,
|
count: data.count,
|
||||||
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [selectedType, history.location.search]),
|
}, [selectedType, history.location.search]),
|
||||||
{ credentials: [], count: 0 }
|
{ credentials: [], count: 0, actions: {}, relatedSearchFields: [] }
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -97,6 +104,11 @@ function CredentialsStep({ i18n }) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{types && types.length > 0 && (
|
{types && types.length > 0 && (
|
||||||
@@ -129,16 +141,16 @@ function CredentialsStep({ i18n }) {
|
|||||||
searchColumns={[
|
searchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
sortColumns={[
|
sortColumns={[
|
||||||
@@ -147,6 +159,8 @@ function CredentialsStep({ i18n }) {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
searchableKeys={searchableKeys}
|
||||||
|
relatedSearchableKeys={relatedSearchableKeys}
|
||||||
multiple={isVault}
|
multiple={isVault}
|
||||||
header={i18n._(t`Credentials`)}
|
header={i18n._(t`Credentials`)}
|
||||||
name="credentials"
|
name="credentials"
|
||||||
|
|||||||
@@ -31,6 +31,15 @@ describe('CredentialsStep', () => {
|
|||||||
count: 5,
|
count: 5,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
CredentialsAPI.readOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {},
|
||||||
|
POST: {},
|
||||||
|
},
|
||||||
|
related_search_fields: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should load credentials', async () => {
|
test('should load credentials', async () => {
|
||||||
|
|||||||
@@ -27,20 +27,29 @@ function InventoryStep({ i18n }) {
|
|||||||
const {
|
const {
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
result: { inventories, count },
|
result: { inventories, count, actions, relatedSearchFields },
|
||||||
request: fetchInventories,
|
request: fetchInventories,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||||
const { data } = await InventoriesAPI.read(params);
|
const [{ data }, actionsResponse] = await Promise.all([
|
||||||
|
InventoriesAPI.read(params),
|
||||||
|
InventoriesAPI.readOptions(),
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
inventories: data.results,
|
inventories: data.results,
|
||||||
count: data.count,
|
count: data.count,
|
||||||
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [history.location]),
|
}, [history.location]),
|
||||||
{
|
{
|
||||||
count: 0,
|
count: 0,
|
||||||
inventories: [],
|
inventories: [],
|
||||||
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -48,6 +57,11 @@ function InventoryStep({ i18n }) {
|
|||||||
fetchInventories();
|
fetchInventories();
|
||||||
}, [fetchInventories]);
|
}, [fetchInventories]);
|
||||||
|
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
@@ -63,16 +77,16 @@ function InventoryStep({ i18n }) {
|
|||||||
searchColumns={[
|
searchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
sortColumns={[
|
sortColumns={[
|
||||||
@@ -81,6 +95,8 @@ function InventoryStep({ i18n }) {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
searchableKeys={searchableKeys}
|
||||||
|
relatedSearchableKeys={relatedSearchableKeys}
|
||||||
header={i18n._(t`Inventory`)}
|
header={i18n._(t`Inventory`)}
|
||||||
name="inventory"
|
name="inventory"
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ describe('InventoryStep', () => {
|
|||||||
count: 3,
|
count: 3,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
InventoriesAPI.readOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {},
|
||||||
|
POST: {},
|
||||||
|
},
|
||||||
|
related_search_fields: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should load inventories', async () => {
|
test('should load inventories', async () => {
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ class ListHeader extends React.Component {
|
|||||||
emptyStateControls,
|
emptyStateControls,
|
||||||
itemCount,
|
itemCount,
|
||||||
searchColumns,
|
searchColumns,
|
||||||
|
searchableKeys,
|
||||||
|
relatedSearchableKeys,
|
||||||
sortColumns,
|
sortColumns,
|
||||||
renderToolbar,
|
renderToolbar,
|
||||||
qsConfig,
|
qsConfig,
|
||||||
@@ -122,6 +124,8 @@ class ListHeader extends React.Component {
|
|||||||
itemCount,
|
itemCount,
|
||||||
searchColumns,
|
searchColumns,
|
||||||
sortColumns,
|
sortColumns,
|
||||||
|
searchableKeys,
|
||||||
|
relatedSearchableKeys,
|
||||||
onSearch: this.handleSearch,
|
onSearch: this.handleSearch,
|
||||||
onReplaceSearch: this.handleReplaceSearch,
|
onReplaceSearch: this.handleReplaceSearch,
|
||||||
onSort: this.handleSort,
|
onSort: this.handleSort,
|
||||||
@@ -141,12 +145,16 @@ ListHeader.propTypes = {
|
|||||||
itemCount: PropTypes.number.isRequired,
|
itemCount: PropTypes.number.isRequired,
|
||||||
qsConfig: QSConfig.isRequired,
|
qsConfig: QSConfig.isRequired,
|
||||||
searchColumns: SearchColumns.isRequired,
|
searchColumns: SearchColumns.isRequired,
|
||||||
|
searchableKeys: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
relatedSearchableKeys: PropTypes.arrayOf(PropTypes.string),
|
||||||
sortColumns: SortColumns.isRequired,
|
sortColumns: SortColumns.isRequired,
|
||||||
renderToolbar: PropTypes.func,
|
renderToolbar: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
ListHeader.defaultProps = {
|
ListHeader.defaultProps = {
|
||||||
renderToolbar: props => <DataListToolbar {...props} />,
|
renderToolbar: props => <DataListToolbar {...props} />,
|
||||||
|
searchableKeys: [],
|
||||||
|
relatedSearchableKeys: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(ListHeader);
|
export default withRouter(ListHeader);
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ describe('ListHeader', () => {
|
|||||||
<ListHeader
|
<ListHeader
|
||||||
itemCount={50}
|
itemCount={50}
|
||||||
qsConfig={qsConfig}
|
qsConfig={qsConfig}
|
||||||
searchColumns={[{ name: 'foo', key: 'foo', isDefault: true }]}
|
searchColumns={[
|
||||||
|
{ name: 'foo', key: 'foo__icontains', isDefault: true },
|
||||||
|
]}
|
||||||
sortColumns={[{ name: 'foo', key: 'foo' }]}
|
sortColumns={[{ name: 'foo', key: 'foo' }]}
|
||||||
renderToolbar={renderToolbarFn}
|
renderToolbar={renderToolbarFn}
|
||||||
/>
|
/>
|
||||||
@@ -33,7 +35,9 @@ describe('ListHeader', () => {
|
|||||||
<ListHeader
|
<ListHeader
|
||||||
itemCount={7}
|
itemCount={7}
|
||||||
qsConfig={qsConfig}
|
qsConfig={qsConfig}
|
||||||
searchColumns={[{ name: 'foo', key: 'foo', isDefault: true }]}
|
searchColumns={[
|
||||||
|
{ name: 'foo', key: 'foo__icontains', isDefault: true },
|
||||||
|
]}
|
||||||
sortColumns={[{ name: 'foo', key: 'foo' }]}
|
sortColumns={[{ name: 'foo', key: 'foo' }]}
|
||||||
/>,
|
/>,
|
||||||
{ context: { router: { history } } }
|
{ context: { router: { history } } }
|
||||||
@@ -56,7 +60,9 @@ describe('ListHeader', () => {
|
|||||||
<ListHeader
|
<ListHeader
|
||||||
itemCount={7}
|
itemCount={7}
|
||||||
qsConfig={qsConfig}
|
qsConfig={qsConfig}
|
||||||
searchColumns={[{ name: 'foo', key: 'foo', isDefault: true }]}
|
searchColumns={[
|
||||||
|
{ name: 'foo', key: 'foo__icontains', isDefault: true },
|
||||||
|
]}
|
||||||
sortColumns={[{ name: 'foo', key: 'foo' }]}
|
sortColumns={[{ name: 'foo', key: 'foo' }]}
|
||||||
/>,
|
/>,
|
||||||
{ context: { router: { history } } }
|
{ context: { router: { history } } }
|
||||||
@@ -77,7 +83,9 @@ describe('ListHeader', () => {
|
|||||||
<ListHeader
|
<ListHeader
|
||||||
itemCount={7}
|
itemCount={7}
|
||||||
qsConfig={qsConfig}
|
qsConfig={qsConfig}
|
||||||
searchColumns={[{ name: 'foo', key: 'foo', isDefault: true }]}
|
searchColumns={[
|
||||||
|
{ name: 'foo', key: 'foo__icontains', isDefault: true },
|
||||||
|
]}
|
||||||
sortColumns={[{ name: 'foo', key: 'foo' }]}
|
sortColumns={[{ name: 'foo', key: 'foo' }]}
|
||||||
/>,
|
/>,
|
||||||
{ context: { router: { history } } }
|
{ context: { router: { history } } }
|
||||||
@@ -100,7 +108,9 @@ describe('ListHeader', () => {
|
|||||||
<ListHeader
|
<ListHeader
|
||||||
itemCount={7}
|
itemCount={7}
|
||||||
qsConfig={qsConfig}
|
qsConfig={qsConfig}
|
||||||
searchColumns={[{ name: 'foo', key: 'foo', isDefault: true }]}
|
searchColumns={[
|
||||||
|
{ name: 'foo', key: 'foo__icontains', isDefault: true },
|
||||||
|
]}
|
||||||
sortColumns={[{ name: 'foo', key: 'foo' }]}
|
sortColumns={[{ name: 'foo', key: 'foo' }]}
|
||||||
/>,
|
/>,
|
||||||
{ context: { router: { history } } }
|
{ context: { router: { history } } }
|
||||||
|
|||||||
@@ -22,22 +22,41 @@ function ApplicationLookup({ i18n, onChange, value, label }) {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const {
|
const {
|
||||||
error,
|
error,
|
||||||
result: { applications, itemCount },
|
result: { applications, itemCount, actions, relatedSearchFields },
|
||||||
request: fetchApplications,
|
request: fetchApplications,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
|
|
||||||
const {
|
const [
|
||||||
data: { results, count },
|
{
|
||||||
} = await ApplicationsAPI.read(params);
|
data: { results, count },
|
||||||
return { applications: results, itemCount: count };
|
},
|
||||||
|
actionsResponse,
|
||||||
|
] = await Promise.all([
|
||||||
|
ApplicationsAPI.read(params),
|
||||||
|
ApplicationsAPI.readOptions,
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
applications: results,
|
||||||
|
itemCount: count,
|
||||||
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
|
};
|
||||||
}, [location]),
|
}, [location]),
|
||||||
{ applications: [], itemCount: 0 }
|
{ applications: [], itemCount: 0, actions: {}, relatedSearchFields: [] }
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchApplications();
|
fetchApplications();
|
||||||
}, [fetchApplications]);
|
}, [fetchApplications]);
|
||||||
|
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup fieldId="application" label={label}>
|
<FormGroup fieldId="application" label={label}>
|
||||||
<Lookup
|
<Lookup
|
||||||
@@ -56,12 +75,12 @@ function ApplicationLookup({ i18n, onChange, value, label }) {
|
|||||||
searchColumns={[
|
searchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Description`),
|
name: i18n._(t`Description`),
|
||||||
key: 'description',
|
key: 'description__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
sortColumns={[
|
sortColumns={[
|
||||||
@@ -82,6 +101,8 @@ function ApplicationLookup({ i18n, onChange, value, label }) {
|
|||||||
key: 'description',
|
key: 'description',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
searchableKeys={searchableKeys}
|
||||||
|
relatedSearchableKeys={relatedSearchableKeys}
|
||||||
readOnly={!canDelete}
|
readOnly={!canDelete}
|
||||||
name="application"
|
name="application"
|
||||||
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
|
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function CredentialLookup({
|
|||||||
tooltip,
|
tooltip,
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
result: { count, credentials },
|
result: { count, credentials, actions, relatedSearchFields },
|
||||||
error,
|
error,
|
||||||
request: fetchCredentials,
|
request: fetchCredentials,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
@@ -51,16 +51,23 @@ function CredentialLookup({
|
|||||||
? { credential_type__namespace: credentialTypeNamespace }
|
? { credential_type__namespace: credentialTypeNamespace }
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
const { data } = await CredentialsAPI.read(
|
const [{ data }, actionsResponse] = await Promise.all([
|
||||||
mergeParams(params, {
|
CredentialsAPI.read(
|
||||||
...typeIdParams,
|
mergeParams(params, {
|
||||||
...typeKindParams,
|
...typeIdParams,
|
||||||
...typeNamespaceParams,
|
...typeKindParams,
|
||||||
})
|
...typeNamespaceParams,
|
||||||
);
|
})
|
||||||
|
),
|
||||||
|
CredentialsAPI.readOptions,
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
count: data.count,
|
count: data.count,
|
||||||
credentials: data.results,
|
credentials: data.results,
|
||||||
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
credentialTypeId,
|
credentialTypeId,
|
||||||
@@ -71,6 +78,8 @@ function CredentialLookup({
|
|||||||
{
|
{
|
||||||
count: 0,
|
count: 0,
|
||||||
credentials: [],
|
credentials: [],
|
||||||
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -78,6 +87,11 @@ function CredentialLookup({
|
|||||||
fetchCredentials();
|
fetchCredentials();
|
||||||
}, [fetchCredentials]);
|
}, [fetchCredentials]);
|
||||||
|
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: replace credential type search with REST-based grabbing of cred types
|
// TODO: replace credential type search with REST-based grabbing of cred types
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -107,16 +121,16 @@ function CredentialLookup({
|
|||||||
searchColumns={[
|
searchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
sortColumns={[
|
sortColumns={[
|
||||||
@@ -125,6 +139,8 @@ function CredentialLookup({
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
searchableKeys={searchableKeys}
|
||||||
|
relatedSearchableKeys={relatedSearchableKeys}
|
||||||
readOnly={!canDelete}
|
readOnly={!canDelete}
|
||||||
name="credential"
|
name="credential"
|
||||||
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
|
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
|
||||||
|
|||||||
@@ -30,26 +30,38 @@ function InstanceGroupsLookup(props) {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { instanceGroups, count },
|
result: { instanceGroups, count, actions, relatedSearchFields },
|
||||||
request: fetchInstanceGroups,
|
request: fetchInstanceGroups,
|
||||||
error,
|
error,
|
||||||
isLoading,
|
isLoading,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||||
const { data } = await InstanceGroupsAPI.read(params);
|
const [{ data }, actionsResponse] = await Promise.all([
|
||||||
|
InstanceGroupsAPI.read(params),
|
||||||
|
InstanceGroupsAPI.readOptions(),
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
instanceGroups: data.results,
|
instanceGroups: data.results,
|
||||||
count: data.count,
|
count: data.count,
|
||||||
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [history.location]),
|
}, [history.location]),
|
||||||
{ instanceGroups: [], count: 0 }
|
{ instanceGroups: [], count: 0, actions: {}, relatedSearchFields: [] }
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchInstanceGroups();
|
fetchInstanceGroups();
|
||||||
}, [fetchInstanceGroups]);
|
}, [fetchInstanceGroups]);
|
||||||
|
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
className={className}
|
className={className}
|
||||||
@@ -74,12 +86,12 @@ function InstanceGroupsLookup(props) {
|
|||||||
searchColumns={[
|
searchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Credential Name`),
|
name: i18n._(t`Credential Name`),
|
||||||
key: 'credential__name',
|
key: 'credential__name__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
sortColumns={[
|
sortColumns={[
|
||||||
@@ -88,6 +100,8 @@ function InstanceGroupsLookup(props) {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
searchableKeys={searchableKeys}
|
||||||
|
relatedSearchableKeys={relatedSearchableKeys}
|
||||||
multiple={state.multiple}
|
multiple={state.multiple}
|
||||||
header={i18n._(t`Instance Groups`)}
|
header={i18n._(t`Instance Groups`)}
|
||||||
name="instanceGroups"
|
name="instanceGroups"
|
||||||
|
|||||||
@@ -19,26 +19,38 @@ const QS_CONFIG = getQSConfig('inventory', {
|
|||||||
|
|
||||||
function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) {
|
function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) {
|
||||||
const {
|
const {
|
||||||
result: { inventories, count },
|
result: { inventories, count, actions, relatedSearchFields },
|
||||||
request: fetchInventories,
|
request: fetchInventories,
|
||||||
error,
|
error,
|
||||||
isLoading,
|
isLoading,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||||
const { data } = await InventoriesAPI.read(params);
|
const [{ data }, actionsResponse] = await Promise.all([
|
||||||
|
InventoriesAPI.read(params),
|
||||||
|
InventoriesAPI.readOptions(),
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
inventories: data.results,
|
inventories: data.results,
|
||||||
count: data.count,
|
count: data.count,
|
||||||
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [history.location]),
|
}, [history.location]),
|
||||||
{ inventories: [], count: 0 }
|
{ inventories: [], count: 0, actions: {}, relatedSearchFields: [] }
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchInventories();
|
fetchInventories();
|
||||||
}, [fetchInventories]);
|
}, [fetchInventories]);
|
||||||
|
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Lookup
|
<Lookup
|
||||||
@@ -58,16 +70,16 @@ function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) {
|
|||||||
searchColumns={[
|
searchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
sortColumns={[
|
sortColumns={[
|
||||||
@@ -76,6 +88,8 @@ function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
searchableKeys={searchableKeys}
|
||||||
|
relatedSearchableKeys={relatedSearchableKeys}
|
||||||
multiple={state.multiple}
|
multiple={state.multiple}
|
||||||
header={i18n._(t`Inventory`)}
|
header={i18n._(t`Inventory`)}
|
||||||
name="inventory"
|
name="inventory"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function MultiCredentialsLookup(props) {
|
|||||||
}, [fetchTypes]);
|
}, [fetchTypes]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { credentials, credentialsCount },
|
result: { credentials, credentialsCount, actions, relatedSearchFields },
|
||||||
request: fetchCredentials,
|
request: fetchCredentials,
|
||||||
error: credentialsError,
|
error: credentialsError,
|
||||||
isLoading: isCredentialsLoading,
|
isLoading: isCredentialsLoading,
|
||||||
@@ -62,15 +62,24 @@ function MultiCredentialsLookup(props) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||||
const { results, count } = await loadCredentials(params, selectedType.id);
|
const [{ results, count }, actionsResponse] = await Promise.all([
|
||||||
|
loadCredentials(params, selectedType.id),
|
||||||
|
CredentialsAPI.readOptions(),
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
credentials: results,
|
credentials: results,
|
||||||
credentialsCount: count,
|
credentialsCount: count,
|
||||||
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [selectedType, history.location]),
|
}, [selectedType, history.location]),
|
||||||
{
|
{
|
||||||
credentials: [],
|
credentials: [],
|
||||||
credentialsCount: 0,
|
credentialsCount: 0,
|
||||||
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -95,6 +104,11 @@ function MultiCredentialsLookup(props) {
|
|||||||
|
|
||||||
const isVault = selectedType?.kind === 'vault';
|
const isVault = selectedType?.kind === 'vault';
|
||||||
|
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Lookup
|
<Lookup
|
||||||
id="multiCredential"
|
id="multiCredential"
|
||||||
@@ -149,16 +163,16 @@ function MultiCredentialsLookup(props) {
|
|||||||
searchColumns={[
|
searchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
sortColumns={[
|
sortColumns={[
|
||||||
@@ -167,6 +181,8 @@ function MultiCredentialsLookup(props) {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
searchableKeys={searchableKeys}
|
||||||
|
relatedSearchableKeys={relatedSearchableKeys}
|
||||||
multiple={isVault}
|
multiple={isVault}
|
||||||
header={i18n._(t`Credentials`)}
|
header={i18n._(t`Credentials`)}
|
||||||
name="credentials"
|
name="credentials"
|
||||||
|
|||||||
@@ -43,6 +43,15 @@ describe('<MultiCredentialsLookup />', () => {
|
|||||||
count: 3,
|
count: 3,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
CredentialsAPI.readOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {},
|
||||||
|
POST: {},
|
||||||
|
},
|
||||||
|
related_search_fields: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -80,16 +80,16 @@ function OrganizationLookup({
|
|||||||
searchColumns={[
|
searchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created by (username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified by (username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
sortColumns={[
|
sortColumns={[
|
||||||
|
|||||||
@@ -32,25 +32,34 @@ function ProjectLookup({
|
|||||||
history,
|
history,
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
result: { projects, count },
|
result: { projects, count, actions, relatedSearchFields },
|
||||||
request: fetchProjects,
|
request: fetchProjects,
|
||||||
error,
|
error,
|
||||||
isLoading,
|
isLoading,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||||
const { data } = await ProjectsAPI.read(params);
|
const [{ data }, actionsResponse] = await Promise.all([
|
||||||
|
ProjectsAPI.read(params),
|
||||||
|
ProjectsAPI.readOptions(),
|
||||||
|
]);
|
||||||
if (data.count === 1 && autocomplete) {
|
if (data.count === 1 && autocomplete) {
|
||||||
autocomplete(data.results[0]);
|
autocomplete(data.results[0]);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
count: data.count,
|
count: data.count,
|
||||||
projects: data.results,
|
projects: data.results,
|
||||||
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [history.location.search, autocomplete]),
|
}, [history.location.search, autocomplete]),
|
||||||
{
|
{
|
||||||
count: 0,
|
count: 0,
|
||||||
projects: [],
|
projects: [],
|
||||||
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -58,6 +67,11 @@ function ProjectLookup({
|
|||||||
fetchProjects();
|
fetchProjects();
|
||||||
}, [fetchProjects]);
|
}, [fetchProjects]);
|
||||||
|
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
fieldId="project"
|
fieldId="project"
|
||||||
@@ -83,12 +97,12 @@ function ProjectLookup({
|
|||||||
searchColumns={[
|
searchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Type`),
|
name: i18n._(t`Type`),
|
||||||
key: 'scm_type',
|
key: 'or__scm_type',
|
||||||
options: [
|
options: [
|
||||||
[``, i18n._(t`Manual`)],
|
[``, i18n._(t`Manual`)],
|
||||||
[`git`, i18n._(t`Git`)],
|
[`git`, i18n._(t`Git`)],
|
||||||
@@ -99,15 +113,15 @@ function ProjectLookup({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Source Control URL`),
|
name: i18n._(t`Source Control URL`),
|
||||||
key: 'scm_url',
|
key: 'scm_url__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
sortColumns={[
|
sortColumns={[
|
||||||
@@ -116,6 +130,8 @@ function ProjectLookup({
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
searchableKeys={searchableKeys}
|
||||||
|
relatedSearchableKeys={relatedSearchableKeys}
|
||||||
options={projects}
|
options={projects}
|
||||||
optionCount={count}
|
optionCount={count}
|
||||||
multiple={state.multiple}
|
multiple={state.multiple}
|
||||||
|
|||||||
@@ -145,12 +145,12 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Type`),
|
name: i18n._(t`Type`),
|
||||||
key: 'type',
|
key: 'or__type',
|
||||||
options: [
|
options: [
|
||||||
['email', i18n._(t`Email`)],
|
['email', i18n._(t`Email`)],
|
||||||
['grafana', i18n._(t`Grafana`)],
|
['grafana', i18n._(t`Grafana`)],
|
||||||
@@ -165,12 +165,12 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created by (username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified by (username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ function OptionsList({
|
|||||||
optionCount,
|
optionCount,
|
||||||
searchColumns,
|
searchColumns,
|
||||||
sortColumns,
|
sortColumns,
|
||||||
|
searchableKeys,
|
||||||
|
relatedSearchableKeys,
|
||||||
multiple,
|
multiple,
|
||||||
header,
|
header,
|
||||||
name,
|
name,
|
||||||
@@ -61,6 +63,8 @@ function OptionsList({
|
|||||||
qsConfig={qsConfig}
|
qsConfig={qsConfig}
|
||||||
toolbarSearchColumns={searchColumns}
|
toolbarSearchColumns={searchColumns}
|
||||||
toolbarSortColumns={sortColumns}
|
toolbarSortColumns={sortColumns}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
hasContentLoading={isLoading}
|
hasContentLoading={isLoading}
|
||||||
onRowClick={selectItem}
|
onRowClick={selectItem}
|
||||||
renderItem={item => (
|
renderItem={item => (
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ describe('<OptionsList />', () => {
|
|||||||
value={[]}
|
value={[]}
|
||||||
options={options}
|
options={options}
|
||||||
optionCount={3}
|
optionCount={3}
|
||||||
searchColumns={[{ name: 'Foo', key: 'foo', isDefault: true }]}
|
searchColumns={[
|
||||||
|
{ name: 'Foo', key: 'foo__icontains', isDefault: true },
|
||||||
|
]}
|
||||||
sortColumns={[{ name: 'Foo', key: 'foo' }]}
|
sortColumns={[{ name: 'Foo', key: 'foo' }]}
|
||||||
qsConfig={qsConfig}
|
qsConfig={qsConfig}
|
||||||
selectItem={() => {}}
|
selectItem={() => {}}
|
||||||
@@ -40,7 +42,9 @@ describe('<OptionsList />', () => {
|
|||||||
value={[options[1]]}
|
value={[options[1]]}
|
||||||
options={options}
|
options={options}
|
||||||
optionCount={3}
|
optionCount={3}
|
||||||
searchColumns={[{ name: 'Foo', key: 'foo', isDefault: true }]}
|
searchColumns={[
|
||||||
|
{ name: 'Foo', key: 'foo__icontains', isDefault: true },
|
||||||
|
]}
|
||||||
sortColumns={[{ name: 'Foo', key: 'foo' }]}
|
sortColumns={[{ name: 'Foo', key: 'foo' }]}
|
||||||
qsConfig={qsConfig}
|
qsConfig={qsConfig}
|
||||||
selectItem={() => {}}
|
selectItem={() => {}}
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ class PaginatedDataList extends React.Component {
|
|||||||
qsConfig,
|
qsConfig,
|
||||||
renderItem,
|
renderItem,
|
||||||
toolbarSearchColumns,
|
toolbarSearchColumns,
|
||||||
|
toolbarSearchableKeys,
|
||||||
|
toolbarRelatedSearchableKeys,
|
||||||
toolbarSortColumns,
|
toolbarSortColumns,
|
||||||
pluralizedItemName,
|
pluralizedItemName,
|
||||||
showPageSizeOptions,
|
showPageSizeOptions,
|
||||||
@@ -151,6 +153,8 @@ class PaginatedDataList extends React.Component {
|
|||||||
emptyStateControls={emptyStateControls}
|
emptyStateControls={emptyStateControls}
|
||||||
searchColumns={searchColumns}
|
searchColumns={searchColumns}
|
||||||
sortColumns={sortColumns}
|
sortColumns={sortColumns}
|
||||||
|
searchableKeys={toolbarSearchableKeys}
|
||||||
|
relatedSearchableKeys={toolbarRelatedSearchableKeys}
|
||||||
qsConfig={qsConfig}
|
qsConfig={qsConfig}
|
||||||
pagination={ToolbarPagination}
|
pagination={ToolbarPagination}
|
||||||
/>
|
/>
|
||||||
@@ -193,6 +197,8 @@ PaginatedDataList.propTypes = {
|
|||||||
qsConfig: QSConfig.isRequired,
|
qsConfig: QSConfig.isRequired,
|
||||||
renderItem: PropTypes.func,
|
renderItem: PropTypes.func,
|
||||||
toolbarSearchColumns: SearchColumns,
|
toolbarSearchColumns: SearchColumns,
|
||||||
|
toolbarSearchableKeys: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
toolbarRelatedSearchableKeys: PropTypes.arrayOf(PropTypes.string),
|
||||||
toolbarSortColumns: SortColumns,
|
toolbarSortColumns: SortColumns,
|
||||||
showPageSizeOptions: PropTypes.bool,
|
showPageSizeOptions: PropTypes.bool,
|
||||||
renderToolbar: PropTypes.func,
|
renderToolbar: PropTypes.func,
|
||||||
@@ -205,6 +211,8 @@ PaginatedDataList.defaultProps = {
|
|||||||
hasContentLoading: false,
|
hasContentLoading: false,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
toolbarSearchColumns: [],
|
toolbarSearchColumns: [],
|
||||||
|
toolbarSearchableKeys: [],
|
||||||
|
toolbarRelatedSearchableKeys: [],
|
||||||
toolbarSortColumns: [],
|
toolbarSortColumns: [],
|
||||||
pluralizedItemName: 'Items',
|
pluralizedItemName: 'Items',
|
||||||
showPageSizeOptions: true,
|
showPageSizeOptions: true,
|
||||||
|
|||||||
@@ -80,16 +80,16 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Username`),
|
name: i18n._(t`Username`),
|
||||||
key: 'username',
|
key: 'username__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`First name`),
|
name: i18n._(t`First Name`),
|
||||||
key: 'first_name',
|
key: 'first_name__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Last name`),
|
name: i18n._(t`Last Name`),
|
||||||
key: 'last_name',
|
key: 'last_name__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
@@ -98,11 +98,11 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
|
|||||||
key: 'username',
|
key: 'username',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`First name`),
|
name: i18n._(t`First Name`),
|
||||||
key: 'first_name',
|
key: 'first_name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Last name`),
|
name: i18n._(t`Last Name`),
|
||||||
key: 'last_name',
|
key: 'last_name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function ScheduleList({
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { schedules, itemCount, actions },
|
result: { schedules, itemCount, actions, relatedSearchFields },
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchSchedules,
|
request: fetchSchedules,
|
||||||
@@ -49,12 +49,16 @@ function ScheduleList({
|
|||||||
schedules: results,
|
schedules: results,
|
||||||
itemCount: count,
|
itemCount: count,
|
||||||
actions: scheduleActions.data.actions,
|
actions: scheduleActions.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
scheduleActions?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [location, loadSchedules, loadScheduleOptions]),
|
}, [location, loadSchedules, loadScheduleOptions]),
|
||||||
{
|
{
|
||||||
schedules: [],
|
schedules: [],
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
actions: {},
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -102,6 +106,10 @@ function ScheduleList({
|
|||||||
actions &&
|
actions &&
|
||||||
Object.prototype.hasOwnProperty.call(actions, 'POST') &&
|
Object.prototype.hasOwnProperty.call(actions, 'POST') &&
|
||||||
!hideAddButton;
|
!hideAddButton;
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -123,7 +131,7 @@ function ScheduleList({
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@@ -141,6 +149,8 @@ function ScheduleList({
|
|||||||
key: 'unified_job_template__polymorphic_ctype__model',
|
key: 'unified_job_template__polymorphic_ctype__model',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderToolbar={props => (
|
renderToolbar={props => (
|
||||||
<DataListToolbar
|
<DataListToolbar
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
276
awx/ui_next/src/components/Search/AdvancedSearch.jsx
Normal file
276
awx/ui_next/src/components/Search/AdvancedSearch.jsx
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import 'styled-components/macro';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonVariant,
|
||||||
|
InputGroup,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
SelectVariant,
|
||||||
|
TextInput,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { SearchIcon } from '@patternfly/react-icons';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const AdvancedGroup = styled.div`
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--pf-c-toolbar__expandable-content--m-expanded--GridRowGap);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function AdvancedSearch({
|
||||||
|
i18n,
|
||||||
|
onSearch,
|
||||||
|
searchableKeys,
|
||||||
|
relatedSearchableKeys,
|
||||||
|
}) {
|
||||||
|
// TODO: blocked by pf bug, eventually separate these into two groups in the select
|
||||||
|
// for now, I'm spreading set to get rid of duplicate keys...when they are grouped
|
||||||
|
// we might want to revisit that.
|
||||||
|
const allKeys = [
|
||||||
|
...new Set([...(searchableKeys || []), ...(relatedSearchableKeys || [])]),
|
||||||
|
];
|
||||||
|
|
||||||
|
const [isPrefixDropdownOpen, setIsPrefixDropdownOpen] = useState(false);
|
||||||
|
const [isLookupDropdownOpen, setIsLookupDropdownOpen] = useState(false);
|
||||||
|
const [isKeyDropdownOpen, setIsKeyDropdownOpen] = useState(false);
|
||||||
|
const [prefixSelection, setPrefixSelection] = useState(null);
|
||||||
|
const [lookupSelection, setLookupSelection] = useState(null);
|
||||||
|
const [keySelection, setKeySelection] = useState(null);
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
|
||||||
|
const handleAdvancedSearch = e => {
|
||||||
|
// keeps page from fully reloading
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (searchValue) {
|
||||||
|
const actualPrefix = prefixSelection === 'and' ? null : prefixSelection;
|
||||||
|
let actualSearchKey;
|
||||||
|
// TODO: once we are able to group options for the key typeahead, we will
|
||||||
|
// probably want to be able to which group a key was clicked in for duplicates,
|
||||||
|
// rather than checking to make sure it's not in both for this appending
|
||||||
|
// __search logic
|
||||||
|
if (
|
||||||
|
relatedSearchableKeys.indexOf(keySelection) > -1 &&
|
||||||
|
searchableKeys.indexOf(keySelection) === -1 &&
|
||||||
|
keySelection.indexOf('__') === -1
|
||||||
|
) {
|
||||||
|
actualSearchKey = `${keySelection}__search`;
|
||||||
|
} else {
|
||||||
|
actualSearchKey = [actualPrefix, keySelection, lookupSelection]
|
||||||
|
.filter(val => !!val)
|
||||||
|
.join('__');
|
||||||
|
}
|
||||||
|
onSearch(actualSearchKey, searchValue);
|
||||||
|
setSearchValue('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdvancedTextKeyDown = e => {
|
||||||
|
if (e.key && e.key === 'Enter') {
|
||||||
|
handleAdvancedSearch(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdvancedGroup>
|
||||||
|
<Select
|
||||||
|
aria-label={i18n._(t`Set type select`)}
|
||||||
|
className="setTypeSelect"
|
||||||
|
variant={SelectVariant.typeahead}
|
||||||
|
typeAheadAriaLabel={i18n._(t`Set type typeahead`)}
|
||||||
|
onToggle={setIsPrefixDropdownOpen}
|
||||||
|
onSelect={(event, selection) => setPrefixSelection(selection)}
|
||||||
|
onClear={() => setPrefixSelection(null)}
|
||||||
|
selections={prefixSelection}
|
||||||
|
isOpen={isPrefixDropdownOpen}
|
||||||
|
placeholderText={i18n._(t`Set type`)}
|
||||||
|
maxHeight="500px"
|
||||||
|
>
|
||||||
|
<SelectOption
|
||||||
|
key="and"
|
||||||
|
value="and"
|
||||||
|
description={i18n._(
|
||||||
|
t`Returns results that satisfy this one as well as other filters. This is the default set type if nothing is selected.`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="or"
|
||||||
|
value="or"
|
||||||
|
description={i18n._(
|
||||||
|
t`Returns results that satisfy this one or any other filters.`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="not"
|
||||||
|
value="not"
|
||||||
|
description={i18n._(
|
||||||
|
t`Returns results that have values other than this one as well as other filters.`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
aria-label={i18n._(t`Key select`)}
|
||||||
|
className="keySelect"
|
||||||
|
variant={SelectVariant.typeahead}
|
||||||
|
typeAheadAriaLabel={i18n._(t`Key typeahead`)}
|
||||||
|
onToggle={setIsKeyDropdownOpen}
|
||||||
|
onSelect={(event, selection) => setKeySelection(selection)}
|
||||||
|
onClear={() => setKeySelection(null)}
|
||||||
|
selections={keySelection}
|
||||||
|
isOpen={isKeyDropdownOpen}
|
||||||
|
placeholderText={i18n._(t`Key`)}
|
||||||
|
isCreatable
|
||||||
|
onCreateOption={setKeySelection}
|
||||||
|
maxHeight="500px"
|
||||||
|
>
|
||||||
|
{allKeys.map(optionKey => (
|
||||||
|
<SelectOption key={optionKey} value={optionKey}>
|
||||||
|
{optionKey}
|
||||||
|
</SelectOption>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
aria-label={i18n._(t`Lookup select`)}
|
||||||
|
className="lookupSelect"
|
||||||
|
variant={SelectVariant.typeahead}
|
||||||
|
typeAheadAriaLabel={i18n._(t`Lookup typeahead`)}
|
||||||
|
onToggle={setIsLookupDropdownOpen}
|
||||||
|
onSelect={(event, selection) => setLookupSelection(selection)}
|
||||||
|
onClear={() => setLookupSelection(null)}
|
||||||
|
selections={lookupSelection}
|
||||||
|
isOpen={isLookupDropdownOpen}
|
||||||
|
placeholderText={i18n._(t`Lookup type`)}
|
||||||
|
maxHeight="500px"
|
||||||
|
>
|
||||||
|
<SelectOption
|
||||||
|
key="exact"
|
||||||
|
value="exact"
|
||||||
|
description={i18n._(
|
||||||
|
t`Exact match (default lookup if not specified).`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="iexact"
|
||||||
|
value="iexact"
|
||||||
|
description={i18n._(t`Case-insensitive version of exact.`)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="contains"
|
||||||
|
value="contains"
|
||||||
|
description={i18n._(t`Field contains value.`)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="icontains"
|
||||||
|
value="icontains"
|
||||||
|
description={i18n._(t`Case-insensitive version of contains`)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="startswith"
|
||||||
|
value="startswith"
|
||||||
|
description={i18n._(t`Field starts with value.`)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="istartswith"
|
||||||
|
value="istartswith"
|
||||||
|
description={i18n._(t`Case-insensitive version of startswith.`)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="endswith"
|
||||||
|
value="endswith"
|
||||||
|
description={i18n._(t`Field ends with value.`)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="iendswith"
|
||||||
|
value="iendswith"
|
||||||
|
description={i18n._(t`Case-insensitive version of endswith.`)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="regex"
|
||||||
|
value="regex"
|
||||||
|
description={i18n._(t`Field matches the given regular expression.`)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="iregex"
|
||||||
|
value="iregex"
|
||||||
|
description={i18n._(t`Case-insensitive version of regex.`)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="gt"
|
||||||
|
value="gt"
|
||||||
|
description={i18n._(t`Greater than comparison.`)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="gte"
|
||||||
|
value="gte"
|
||||||
|
description={i18n._(t`Greater than or equal to comparison.`)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="lt"
|
||||||
|
value="lt"
|
||||||
|
description={i18n._(t`Less than comparison.`)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="lte"
|
||||||
|
value="lte"
|
||||||
|
description={i18n._(t`Less than or equal to comparison.`)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="isnull"
|
||||||
|
value="isnull"
|
||||||
|
description={i18n._(
|
||||||
|
t`Check whether the given field or related object is null; expects a boolean value.`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<SelectOption
|
||||||
|
key="in"
|
||||||
|
value="in"
|
||||||
|
description={i18n._(
|
||||||
|
t`Check whether the given field's value is present in the list provided; expects a comma-separated list of items.`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Select>
|
||||||
|
<InputGroup>
|
||||||
|
<TextInput
|
||||||
|
type="search"
|
||||||
|
aria-label={i18n._(t`Advanced search value input`)}
|
||||||
|
isDisabled={!keySelection}
|
||||||
|
value={
|
||||||
|
(!keySelection && i18n._(t`First, select a key`)) || searchValue
|
||||||
|
}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
onKeyDown={handleAdvancedTextKeyDown}
|
||||||
|
/>
|
||||||
|
<div css={!searchValue && `cursor:not-allowed`}>
|
||||||
|
<Button
|
||||||
|
variant={ButtonVariant.control}
|
||||||
|
isDisabled={!searchValue}
|
||||||
|
aria-label={i18n._(t`Search submit button`)}
|
||||||
|
onClick={handleAdvancedSearch}
|
||||||
|
>
|
||||||
|
<SearchIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</InputGroup>
|
||||||
|
</AdvancedGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AdvancedSearch.propTypes = {
|
||||||
|
onSearch: PropTypes.func.isRequired,
|
||||||
|
searchableKeys: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
relatedSearchableKeys: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
};
|
||||||
|
|
||||||
|
AdvancedSearch.defaultProps = {
|
||||||
|
searchableKeys: [],
|
||||||
|
relatedSearchableKeys: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(AdvancedSearch);
|
||||||
342
awx/ui_next/src/components/Search/AdvancedSearch.test.jsx
Normal file
342
awx/ui_next/src/components/Search/AdvancedSearch.test.jsx
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
|
import AdvancedSearch from './AdvancedSearch';
|
||||||
|
|
||||||
|
describe('<AdvancedSearch />', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initially renders without crashing', () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<AdvancedSearch
|
||||||
|
onSearch={jest.fn}
|
||||||
|
searchableKeys={[]}
|
||||||
|
relatedSearchableKeys={[]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Remove duplicates from searchableKeys/relatedSearchableKeys list', () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<AdvancedSearch
|
||||||
|
onSearch={jest.fn}
|
||||||
|
searchableKeys={['foo', 'bar']}
|
||||||
|
relatedSearchableKeys={['bar', 'baz']}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
wrapper
|
||||||
|
.find('Select[aria-label="Key select"] SelectToggle')
|
||||||
|
.simulate('click');
|
||||||
|
expect(
|
||||||
|
wrapper.find('Select[aria-label="Key select"] SelectOption')
|
||||||
|
).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Don't call onSearch unless a search value is set", () => {
|
||||||
|
const advancedSearchMock = jest.fn();
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<AdvancedSearch
|
||||||
|
onSearch={advancedSearchMock}
|
||||||
|
searchableKeys={['foo', 'bar']}
|
||||||
|
relatedSearchableKeys={['bar', 'baz']}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
wrapper
|
||||||
|
.find('Select[aria-label="Key select"] SelectToggle')
|
||||||
|
.simulate('click');
|
||||||
|
wrapper
|
||||||
|
.find('Select[aria-label="Key select"] SelectOption')
|
||||||
|
.at(1)
|
||||||
|
.simulate('click');
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.prop('onKeyDown')({ key: 'Enter', preventDefault: jest.fn });
|
||||||
|
expect(advancedSearchMock).toBeCalledTimes(0);
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.invoke('onChange')('foo');
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.prop('onKeyDown')({ key: 'Enter', preventDefault: jest.fn });
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(advancedSearchMock).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Disable searchValue input until a key is set', () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<AdvancedSearch
|
||||||
|
onSearch={jest.fn}
|
||||||
|
searchableKeys={[]}
|
||||||
|
relatedSearchableKeys={[]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.prop('isDisabled')
|
||||||
|
).toBe(true);
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('Select[aria-label="Key select"]').invoke('onCreateOption')(
|
||||||
|
'foo'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.prop('isDisabled')
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Strip and__ set type from key', () => {
|
||||||
|
const advancedSearchMock = jest.fn();
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<AdvancedSearch
|
||||||
|
onSearch={advancedSearchMock}
|
||||||
|
searchableKeys={[]}
|
||||||
|
relatedSearchableKeys={[]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('Select[aria-label="Set type select"]').invoke('onSelect')(
|
||||||
|
{},
|
||||||
|
'and'
|
||||||
|
);
|
||||||
|
wrapper.find('Select[aria-label="Key select"]').invoke('onCreateOption')(
|
||||||
|
'foo'
|
||||||
|
);
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.invoke('onChange')('bar');
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.prop('onKeyDown')({ key: 'Enter', preventDefault: jest.fn });
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(advancedSearchMock).toBeCalledWith('foo', 'bar');
|
||||||
|
jest.clearAllMocks();
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('Select[aria-label="Set type select"]').invoke('onSelect')(
|
||||||
|
{},
|
||||||
|
'or'
|
||||||
|
);
|
||||||
|
wrapper.find('Select[aria-label="Key select"]').invoke('onCreateOption')(
|
||||||
|
'foo'
|
||||||
|
);
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.invoke('onChange')('bar');
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.prop('onKeyDown')({ key: 'Enter', preventDefault: jest.fn });
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(advancedSearchMock).toBeCalledWith('or__foo', 'bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Add __search lookup to key when applicable', () => {
|
||||||
|
const advancedSearchMock = jest.fn();
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<AdvancedSearch
|
||||||
|
onSearch={advancedSearchMock}
|
||||||
|
searchableKeys={['foo', 'bar']}
|
||||||
|
relatedSearchableKeys={['bar', 'baz']}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('Select[aria-label="Key select"]').invoke('onCreateOption')(
|
||||||
|
'foo'
|
||||||
|
);
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.invoke('onChange')('bar');
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.prop('onKeyDown')({ key: 'Enter', preventDefault: jest.fn });
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(advancedSearchMock).toBeCalledWith('foo', 'bar');
|
||||||
|
jest.clearAllMocks();
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('Select[aria-label="Key select"]').invoke('onCreateOption')(
|
||||||
|
'bar'
|
||||||
|
);
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.invoke('onChange')('bar');
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.prop('onKeyDown')({ key: 'Enter', preventDefault: jest.fn });
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(advancedSearchMock).toBeCalledWith('bar', 'bar');
|
||||||
|
jest.clearAllMocks();
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('Select[aria-label="Key select"]').invoke('onCreateOption')(
|
||||||
|
'baz'
|
||||||
|
);
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.invoke('onChange')('bar');
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.prop('onKeyDown')({ key: 'Enter', preventDefault: jest.fn });
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(advancedSearchMock).toBeCalledWith('baz__search', 'bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Key should be properly constructed from three typeaheads', () => {
|
||||||
|
const advancedSearchMock = jest.fn();
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<AdvancedSearch
|
||||||
|
onSearch={advancedSearchMock}
|
||||||
|
searchableKeys={[]}
|
||||||
|
relatedSearchableKeys={[]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('Select[aria-label="Set type select"]').invoke('onSelect')(
|
||||||
|
{},
|
||||||
|
'or'
|
||||||
|
);
|
||||||
|
wrapper.find('Select[aria-label="Key select"]').invoke('onSelect')(
|
||||||
|
{},
|
||||||
|
'foo'
|
||||||
|
);
|
||||||
|
wrapper.find('Select[aria-label="Lookup select"]').invoke('onSelect')(
|
||||||
|
{},
|
||||||
|
'exact'
|
||||||
|
);
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.invoke('onChange')('bar');
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.prop('onKeyDown')({ key: 'Enter', preventDefault: jest.fn });
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(advancedSearchMock).toBeCalledWith('or__foo__exact', 'bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('searchValue should clear after onSearch is called', () => {
|
||||||
|
const advancedSearchMock = jest.fn();
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<AdvancedSearch
|
||||||
|
onSearch={advancedSearchMock}
|
||||||
|
searchableKeys={[]}
|
||||||
|
relatedSearchableKeys={[]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('Select[aria-label="Set type select"]').invoke('onSelect')(
|
||||||
|
{},
|
||||||
|
'or'
|
||||||
|
);
|
||||||
|
wrapper.find('Select[aria-label="Key select"]').invoke('onCreateOption')(
|
||||||
|
'foo'
|
||||||
|
);
|
||||||
|
wrapper.find('Select[aria-label="Lookup select"]').invoke('onSelect')(
|
||||||
|
{},
|
||||||
|
'exact'
|
||||||
|
);
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.invoke('onChange')('bar');
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.prop('onKeyDown')({ key: 'Enter', preventDefault: jest.fn });
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(advancedSearchMock).toBeCalledWith('or__foo__exact', 'bar');
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.prop('value')
|
||||||
|
).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('typeahead onClear should remove key components', () => {
|
||||||
|
const advancedSearchMock = jest.fn();
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<AdvancedSearch
|
||||||
|
onSearch={advancedSearchMock}
|
||||||
|
searchableKeys={[]}
|
||||||
|
relatedSearchableKeys={[]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('Select[aria-label="Set type select"]').invoke('onSelect')(
|
||||||
|
{},
|
||||||
|
'or'
|
||||||
|
);
|
||||||
|
wrapper.find('Select[aria-label="Key select"]').invoke('onCreateOption')(
|
||||||
|
'foo'
|
||||||
|
);
|
||||||
|
wrapper.find('Select[aria-label="Lookup select"]').invoke('onSelect')(
|
||||||
|
{},
|
||||||
|
'exact'
|
||||||
|
);
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.invoke('onChange')('bar');
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.prop('onKeyDown')({ key: 'Enter', preventDefault: jest.fn });
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(advancedSearchMock).toBeCalledWith('or__foo__exact', 'bar');
|
||||||
|
jest.clearAllMocks();
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('Select[aria-label="Set type select"]').invoke('onClear')();
|
||||||
|
wrapper.find('Select[aria-label="Key select"]').invoke('onClear')();
|
||||||
|
wrapper.find('Select[aria-label="Lookup select"]').invoke('onClear')();
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.invoke('onChange')('baz');
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('TextInputBase[aria-label="Advanced search value input"]')
|
||||||
|
.prop('onKeyDown')({ key: 'Enter', preventDefault: jest.fn });
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(advancedSearchMock).toBeCalledWith('', 'baz');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'styled-components/macro';
|
import 'styled-components/macro';
|
||||||
import React, { Fragment } from 'react';
|
import React, { Fragment, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -7,10 +7,6 @@ import { withRouter } from 'react-router-dom';
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonVariant,
|
ButtonVariant,
|
||||||
Dropdown,
|
|
||||||
DropdownPosition,
|
|
||||||
DropdownToggle,
|
|
||||||
DropdownItem,
|
|
||||||
InputGroup,
|
InputGroup,
|
||||||
Select,
|
Select,
|
||||||
SelectOption,
|
SelectOption,
|
||||||
@@ -24,6 +20,7 @@ import { SearchIcon } from '@patternfly/react-icons';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { parseQueryString } from '../../util/qs';
|
import { parseQueryString } from '../../util/qs';
|
||||||
import { QSConfig, SearchColumns } from '../../types';
|
import { QSConfig, SearchColumns } from '../../types';
|
||||||
|
import AdvancedSearch from './AdvancedSearch';
|
||||||
|
|
||||||
const NoOptionDropdown = styled.div`
|
const NoOptionDropdown = styled.div`
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
@@ -33,288 +30,267 @@ const NoOptionDropdown = styled.div`
|
|||||||
border-bottom-color: var(--pf-global--BorderColor--200);
|
border-bottom-color: var(--pf-global--BorderColor--200);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
class Search extends React.Component {
|
function Search({
|
||||||
constructor(props) {
|
columns,
|
||||||
super(props);
|
i18n,
|
||||||
|
onSearch,
|
||||||
|
onReplaceSearch,
|
||||||
|
onRemove,
|
||||||
|
qsConfig,
|
||||||
|
location,
|
||||||
|
searchableKeys,
|
||||||
|
relatedSearchableKeys,
|
||||||
|
}) {
|
||||||
|
const [isSearchDropdownOpen, setIsSearchDropdownOpen] = useState(false);
|
||||||
|
const [searchKey, setSearchKey] = useState(
|
||||||
|
(() => {
|
||||||
|
const defaultColumn = columns.filter(col => col.isDefault);
|
||||||
|
|
||||||
const { columns } = this.props;
|
if (defaultColumn.length !== 1) {
|
||||||
|
throw new Error(
|
||||||
|
'One (and only one) searchColumn must be marked isDefault: true'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
return defaultColumn[0]?.key;
|
||||||
isSearchDropdownOpen: false,
|
})()
|
||||||
searchKey: columns.find(col => col.isDefault).key,
|
);
|
||||||
searchValue: '',
|
const [searchValue, setSearchValue] = useState('');
|
||||||
isFilterDropdownOpen: false,
|
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
|
||||||
};
|
|
||||||
|
|
||||||
this.handleSearchInputChange = this.handleSearchInputChange.bind(this);
|
const handleDropdownSelect = ({ target }) => {
|
||||||
this.handleDropdownToggle = this.handleDropdownToggle.bind(this);
|
const { key: actualSearchKey } = columns.find(
|
||||||
this.handleDropdownSelect = this.handleDropdownSelect.bind(this);
|
({ name }) => name === target.innerText
|
||||||
this.handleSearch = this.handleSearch.bind(this);
|
|
||||||
this.handleTextKeyDown = this.handleTextKeyDown.bind(this);
|
|
||||||
this.handleFilterDropdownToggle = this.handleFilterDropdownToggle.bind(
|
|
||||||
this
|
|
||||||
);
|
);
|
||||||
this.handleFilterDropdownSelect = this.handleFilterDropdownSelect.bind(
|
|
||||||
this
|
|
||||||
);
|
|
||||||
this.handleFilterBooleanSelect = this.handleFilterBooleanSelect.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDropdownToggle(isSearchDropdownOpen) {
|
setIsFilterDropdownOpen(false);
|
||||||
this.setState({ isSearchDropdownOpen });
|
setSearchKey(actualSearchKey);
|
||||||
}
|
};
|
||||||
|
|
||||||
handleDropdownSelect({ target }) {
|
const handleSearch = e => {
|
||||||
const { columns } = this.props;
|
|
||||||
const { innerText } = target;
|
|
||||||
|
|
||||||
const { key: searchKey } = columns.find(({ name }) => name === innerText);
|
|
||||||
this.setState({ isSearchDropdownOpen: false, searchKey });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSearch(e) {
|
|
||||||
// keeps page from fully reloading
|
// keeps page from fully reloading
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const { searchKey, searchValue } = this.state;
|
|
||||||
const { onSearch, qsConfig } = this.props;
|
|
||||||
|
|
||||||
if (searchValue) {
|
if (searchValue) {
|
||||||
const isNonStringField =
|
onSearch(searchKey, searchValue);
|
||||||
qsConfig.integerFields.find(field => field === searchKey) ||
|
setSearchValue('');
|
||||||
qsConfig.dateFields.find(field => field === searchKey);
|
|
||||||
|
|
||||||
const actualSearchKey = isNonStringField
|
|
||||||
? searchKey
|
|
||||||
: `${searchKey}__icontains`;
|
|
||||||
|
|
||||||
onSearch(actualSearchKey, searchValue);
|
|
||||||
|
|
||||||
this.setState({ searchValue: '' });
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
handleSearchInputChange(searchValue) {
|
const handleTextKeyDown = e => {
|
||||||
this.setState({ searchValue });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTextKeyDown(e) {
|
|
||||||
if (e.key && e.key === 'Enter') {
|
if (e.key && e.key === 'Enter') {
|
||||||
this.handleSearch(e);
|
handleSearch(e);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
handleFilterDropdownToggle(isFilterDropdownOpen) {
|
|
||||||
this.setState({ isFilterDropdownOpen });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFilterDropdownSelect(key, event, actualValue) {
|
|
||||||
const { onSearch, onRemove } = this.props;
|
|
||||||
|
|
||||||
|
const handleFilterDropdownSelect = (key, event, actualValue) => {
|
||||||
if (event.target.checked) {
|
if (event.target.checked) {
|
||||||
onSearch(`or__${key}`, actualValue);
|
onSearch(key, actualValue);
|
||||||
} else {
|
} else {
|
||||||
onRemove(`or__${key}`, actualValue);
|
onRemove(key, actualValue);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
handleFilterBooleanSelect(key, selection) {
|
const filterDefaultParams = (paramsArr, config) => {
|
||||||
const { onReplaceSearch } = this.props;
|
const defaultParamsKeys = Object.keys(config.defaultParams || {});
|
||||||
onReplaceSearch(key, selection);
|
return paramsArr.filter(key => defaultParamsKeys.indexOf(key) === -1);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
const getLabelFromValue = (value, colKey) => {
|
||||||
const { up } = DropdownPosition;
|
const currentSearchColumn = columns.find(({ key }) => key === colKey);
|
||||||
const { columns, i18n, onRemove, qsConfig, location } = this.props;
|
if (currentSearchColumn?.options?.length) {
|
||||||
const {
|
return currentSearchColumn.options.find(
|
||||||
isSearchDropdownOpen,
|
([optVal]) => optVal === value
|
||||||
searchKey,
|
)[1];
|
||||||
searchValue,
|
}
|
||||||
isFilterDropdownOpen,
|
return value.toString();
|
||||||
} = this.state;
|
};
|
||||||
const { name: searchColumnName } = columns.find(
|
|
||||||
({ key }) => key === searchKey
|
const getChipsByKey = () => {
|
||||||
|
const queryParams = parseQueryString(qsConfig, location.search);
|
||||||
|
|
||||||
|
const queryParamsByKey = {};
|
||||||
|
columns.forEach(({ name, key }) => {
|
||||||
|
queryParamsByKey[key] = { key, label: name, chips: [] };
|
||||||
|
});
|
||||||
|
const nonDefaultParams = filterDefaultParams(
|
||||||
|
Object.keys(queryParams || {}),
|
||||||
|
qsConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchDropdownItems = columns
|
nonDefaultParams.forEach(key => {
|
||||||
.filter(({ key }) => key !== searchKey)
|
const columnKey = key;
|
||||||
.map(({ key, name }) => (
|
const label = columns.filter(
|
||||||
<DropdownItem key={key} component="button">
|
({ key: keyToCheck }) => columnKey === keyToCheck
|
||||||
{name}
|
).length
|
||||||
</DropdownItem>
|
? `${
|
||||||
));
|
columns.find(({ key: keyToCheck }) => columnKey === keyToCheck).name
|
||||||
|
} (${key})`
|
||||||
|
: columnKey;
|
||||||
|
|
||||||
const filterDefaultParams = (paramsArr, config) => {
|
queryParamsByKey[columnKey] = { key, label, chips: [] };
|
||||||
const defaultParamsKeys = Object.keys(config.defaultParams || {});
|
|
||||||
return paramsArr.filter(key => defaultParamsKeys.indexOf(key) === -1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLabelFromValue = (value, colKey) => {
|
if (Array.isArray(queryParams[key])) {
|
||||||
const currentSearchColumn = columns.find(({ key }) => key === colKey);
|
queryParams[key].forEach(val =>
|
||||||
if (currentSearchColumn?.options?.length) {
|
|
||||||
return currentSearchColumn.options.find(
|
|
||||||
([optVal]) => optVal === value
|
|
||||||
)[1];
|
|
||||||
}
|
|
||||||
return value.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getChipsByKey = () => {
|
|
||||||
const queryParams = parseQueryString(qsConfig, location.search);
|
|
||||||
|
|
||||||
const queryParamsByKey = {};
|
|
||||||
columns.forEach(({ name, key }) => {
|
|
||||||
queryParamsByKey[key] = { key, label: name, chips: [] };
|
|
||||||
});
|
|
||||||
const nonDefaultParams = filterDefaultParams(
|
|
||||||
Object.keys(queryParams || {}),
|
|
||||||
qsConfig
|
|
||||||
);
|
|
||||||
|
|
||||||
nonDefaultParams.forEach(key => {
|
|
||||||
const columnKey = key.replace('__icontains', '').replace('or__', '');
|
|
||||||
const label = columns.filter(
|
|
||||||
({ key: keyToCheck }) => columnKey === keyToCheck
|
|
||||||
).length
|
|
||||||
? columns.filter(({ key: keyToCheck }) => columnKey === keyToCheck)[0]
|
|
||||||
.name
|
|
||||||
: columnKey;
|
|
||||||
|
|
||||||
queryParamsByKey[columnKey] = { key, label, chips: [] };
|
|
||||||
|
|
||||||
if (Array.isArray(queryParams[key])) {
|
|
||||||
queryParams[key].forEach(val =>
|
|
||||||
queryParamsByKey[columnKey].chips.push({
|
|
||||||
key: `${key}:${val}`,
|
|
||||||
node: getLabelFromValue(val, columnKey),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
queryParamsByKey[columnKey].chips.push({
|
queryParamsByKey[columnKey].chips.push({
|
||||||
key: `${key}:${queryParams[key]}`,
|
key: `${key}:${val}`,
|
||||||
node: getLabelFromValue(queryParams[key], columnKey),
|
node: getLabelFromValue(val, columnKey),
|
||||||
});
|
})
|
||||||
}
|
);
|
||||||
});
|
} else {
|
||||||
|
queryParamsByKey[columnKey].chips.push({
|
||||||
|
key: `${key}:${queryParams[key]}`,
|
||||||
|
node: getLabelFromValue(queryParams[key], columnKey),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return queryParamsByKey;
|
||||||
|
};
|
||||||
|
|
||||||
return queryParamsByKey;
|
const chipsByKey = getChipsByKey();
|
||||||
};
|
|
||||||
|
|
||||||
const chipsByKey = getChipsByKey();
|
const { name: searchColumnName } = columns.find(
|
||||||
|
({ key }) => key === searchKey
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
const searchOptions = columns
|
||||||
<ToolbarGroup variant="filter-group">
|
.filter(({ key }) => key !== searchKey)
|
||||||
<ToolbarItem>
|
.map(({ key, name }) => (
|
||||||
{searchDropdownItems.length > 0 ? (
|
<SelectOption key={key} value={name}>
|
||||||
<Dropdown
|
{name}
|
||||||
onToggle={this.handleDropdownToggle}
|
</SelectOption>
|
||||||
onSelect={this.handleDropdownSelect}
|
));
|
||||||
direction={up}
|
|
||||||
toggle={
|
return (
|
||||||
<DropdownToggle
|
<ToolbarGroup variant="filter-group">
|
||||||
id="awx-search"
|
<ToolbarItem>
|
||||||
onToggle={this.handleDropdownToggle}
|
{searchOptions.length > 0 ? (
|
||||||
style={{ width: '100%' }}
|
<Select
|
||||||
>
|
variant={SelectVariant.single}
|
||||||
{searchColumnName}
|
className="simpleKeySelect"
|
||||||
</DropdownToggle>
|
aria-label={i18n._(t`Simple key select`)}
|
||||||
}
|
onToggle={setIsSearchDropdownOpen}
|
||||||
isOpen={isSearchDropdownOpen}
|
onSelect={handleDropdownSelect}
|
||||||
dropdownItems={searchDropdownItems}
|
selections={searchColumnName}
|
||||||
/>
|
isOpen={isSearchDropdownOpen}
|
||||||
) : (
|
>
|
||||||
<NoOptionDropdown>{searchColumnName}</NoOptionDropdown>
|
{searchOptions}
|
||||||
)}
|
</Select>
|
||||||
</ToolbarItem>
|
) : (
|
||||||
{columns.map(
|
<NoOptionDropdown>{searchColumnName}</NoOptionDropdown>
|
||||||
({ key, name, options, isBoolean, booleanLabels = {} }) => (
|
|
||||||
<ToolbarFilter
|
|
||||||
chips={chipsByKey[key] ? chipsByKey[key].chips : []}
|
|
||||||
deleteChip={(unusedKey, chip) => {
|
|
||||||
const [columnKey, ...value] = chip.key.split(':');
|
|
||||||
onRemove(columnKey, value.join(':'));
|
|
||||||
}}
|
|
||||||
categoryName={chipsByKey[key] ? chipsByKey[key].label : key}
|
|
||||||
key={key}
|
|
||||||
showToolbarItem={searchKey === key}
|
|
||||||
>
|
|
||||||
{(options && (
|
|
||||||
<Fragment>
|
|
||||||
<Select
|
|
||||||
variant={SelectVariant.checkbox}
|
|
||||||
aria-label={name}
|
|
||||||
onToggle={this.handleFilterDropdownToggle}
|
|
||||||
onSelect={(event, selection) =>
|
|
||||||
this.handleFilterDropdownSelect(key, event, selection)
|
|
||||||
}
|
|
||||||
selections={chipsByKey[key].chips.map(chip => {
|
|
||||||
const [, ...value] = chip.key.split(':');
|
|
||||||
return value.join(':');
|
|
||||||
})}
|
|
||||||
isOpen={isFilterDropdownOpen}
|
|
||||||
placeholderText={`Filter By ${name}`}
|
|
||||||
>
|
|
||||||
{options.map(([optionKey, optionLabel]) => (
|
|
||||||
<SelectOption key={optionKey} value={optionKey}>
|
|
||||||
{optionLabel}
|
|
||||||
</SelectOption>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Fragment>
|
|
||||||
)) ||
|
|
||||||
(isBoolean && (
|
|
||||||
<Select
|
|
||||||
aria-label={name}
|
|
||||||
onToggle={this.handleFilterDropdownToggle}
|
|
||||||
onSelect={(event, selection) =>
|
|
||||||
this.handleFilterBooleanSelect(key, selection)
|
|
||||||
}
|
|
||||||
selections={chipsByKey[key].chips[0]}
|
|
||||||
isOpen={isFilterDropdownOpen}
|
|
||||||
placeholderText={`Filter By ${name}`}
|
|
||||||
>
|
|
||||||
<SelectOption key="true" value="true">
|
|
||||||
{booleanLabels.true || i18n._(t`Yes`)}
|
|
||||||
</SelectOption>
|
|
||||||
<SelectOption key="false" value="false">
|
|
||||||
{booleanLabels.false || i18n._(t`No`)}
|
|
||||||
</SelectOption>
|
|
||||||
</Select>
|
|
||||||
)) || (
|
|
||||||
<InputGroup>
|
|
||||||
{/* TODO: add support for dates:
|
|
||||||
qsConfig.dateFields.filter(field => field === key).length && "date" */}
|
|
||||||
<TextInput
|
|
||||||
type={
|
|
||||||
(qsConfig.integerFields.find(
|
|
||||||
field => field === searchKey
|
|
||||||
) &&
|
|
||||||
'number') ||
|
|
||||||
'search'
|
|
||||||
}
|
|
||||||
aria-label={i18n._(t`Search text input`)}
|
|
||||||
value={searchValue}
|
|
||||||
onChange={this.handleSearchInputChange}
|
|
||||||
onKeyDown={this.handleTextKeyDown}
|
|
||||||
/>
|
|
||||||
<div css={!searchValue && `cursor:not-allowed`}>
|
|
||||||
<Button
|
|
||||||
variant={ButtonVariant.control}
|
|
||||||
isDisabled={!searchValue}
|
|
||||||
aria-label={i18n._(t`Search submit button`)}
|
|
||||||
onClick={this.handleSearch}
|
|
||||||
>
|
|
||||||
<SearchIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</InputGroup>
|
|
||||||
)}
|
|
||||||
</ToolbarFilter>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</ToolbarGroup>
|
</ToolbarItem>
|
||||||
);
|
{columns.map(({ key, name, options, isBoolean, booleanLabels = {} }) => (
|
||||||
}
|
<ToolbarFilter
|
||||||
|
chips={chipsByKey[key] ? chipsByKey[key].chips : []}
|
||||||
|
deleteChip={(unusedKey, chip) => {
|
||||||
|
const [columnKey, ...value] = chip.key.split(':');
|
||||||
|
onRemove(columnKey, value.join(':'));
|
||||||
|
}}
|
||||||
|
categoryName={chipsByKey[key] ? chipsByKey[key].label : key}
|
||||||
|
key={key}
|
||||||
|
showToolbarItem={searchKey === key}
|
||||||
|
>
|
||||||
|
{(key === 'advanced' && (
|
||||||
|
<AdvancedSearch
|
||||||
|
onSearch={onSearch}
|
||||||
|
searchableKeys={searchableKeys}
|
||||||
|
relatedSearchableKeys={relatedSearchableKeys}
|
||||||
|
/>
|
||||||
|
)) ||
|
||||||
|
(options && (
|
||||||
|
<Fragment>
|
||||||
|
<Select
|
||||||
|
variant={SelectVariant.checkbox}
|
||||||
|
aria-label={name}
|
||||||
|
onToggle={setIsFilterDropdownOpen}
|
||||||
|
onSelect={(event, selection) =>
|
||||||
|
handleFilterDropdownSelect(key, event, selection)
|
||||||
|
}
|
||||||
|
selections={chipsByKey[key].chips.map(chip => {
|
||||||
|
const [, ...value] = chip.key.split(':');
|
||||||
|
return value.join(':');
|
||||||
|
})}
|
||||||
|
isOpen={isFilterDropdownOpen}
|
||||||
|
placeholderText={`Filter By ${name}`}
|
||||||
|
>
|
||||||
|
{options.map(([optionKey, optionLabel]) => (
|
||||||
|
<SelectOption key={optionKey} value={optionKey}>
|
||||||
|
{optionLabel}
|
||||||
|
</SelectOption>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Fragment>
|
||||||
|
)) ||
|
||||||
|
(isBoolean && (
|
||||||
|
<Select
|
||||||
|
aria-label={name}
|
||||||
|
onToggle={setIsFilterDropdownOpen}
|
||||||
|
onSelect={(event, selection) => onReplaceSearch(key, selection)}
|
||||||
|
selections={chipsByKey[key].chips[0]}
|
||||||
|
isOpen={isFilterDropdownOpen}
|
||||||
|
placeholderText={`Filter By ${name}`}
|
||||||
|
>
|
||||||
|
<SelectOption key="true" value="true">
|
||||||
|
{booleanLabels.true || i18n._(t`Yes`)}
|
||||||
|
</SelectOption>
|
||||||
|
<SelectOption key="false" value="false">
|
||||||
|
{booleanLabels.false || i18n._(t`No`)}
|
||||||
|
</SelectOption>
|
||||||
|
</Select>
|
||||||
|
)) || (
|
||||||
|
<InputGroup>
|
||||||
|
{/* TODO: add support for dates:
|
||||||
|
qsConfig.dateFields.filter(field => field === key).length && "date" */}
|
||||||
|
<TextInput
|
||||||
|
type={
|
||||||
|
(qsConfig.integerFields.find(
|
||||||
|
field => field === searchKey
|
||||||
|
) &&
|
||||||
|
'number') ||
|
||||||
|
'search'
|
||||||
|
}
|
||||||
|
aria-label={i18n._(t`Search text input`)}
|
||||||
|
value={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
onKeyDown={handleTextKeyDown}
|
||||||
|
/>
|
||||||
|
<div css={!searchValue && `cursor:not-allowed`}>
|
||||||
|
<Button
|
||||||
|
variant={ButtonVariant.control}
|
||||||
|
isDisabled={!searchValue}
|
||||||
|
aria-label={i18n._(t`Search submit button`)}
|
||||||
|
onClick={handleSearch}
|
||||||
|
>
|
||||||
|
<SearchIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</InputGroup>
|
||||||
|
)}
|
||||||
|
</ToolbarFilter>
|
||||||
|
))}
|
||||||
|
{/* Add a ToolbarFilter for any key that doesn't have it's own
|
||||||
|
search column so the chips show up */}
|
||||||
|
{Object.keys(chipsByKey)
|
||||||
|
.filter(val => chipsByKey[val].chips.length > 0)
|
||||||
|
.filter(val => columns.map(val2 => val2.key).indexOf(val) === -1)
|
||||||
|
.map(leftoverKey => (
|
||||||
|
<ToolbarFilter
|
||||||
|
chips={chipsByKey[leftoverKey] ? chipsByKey[leftoverKey].chips : []}
|
||||||
|
deleteChip={(unusedKey, chip) => {
|
||||||
|
const [columnKey, ...value] = chip.key.split(':');
|
||||||
|
onRemove(columnKey, value.join(':'));
|
||||||
|
}}
|
||||||
|
categoryName={
|
||||||
|
chipsByKey[leftoverKey]
|
||||||
|
? chipsByKey[leftoverKey].label
|
||||||
|
: leftoverKey
|
||||||
|
}
|
||||||
|
key={leftoverKey}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ToolbarGroup>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Search.propTypes = {
|
Search.propTypes = {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe('<Search />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it triggers the expected callbacks', () => {
|
test('it triggers the expected callbacks', () => {
|
||||||
const columns = [{ name: 'Name', key: 'name', isDefault: true }];
|
const columns = [{ name: 'Name', key: 'name__icontains', isDefault: true }];
|
||||||
|
|
||||||
const searchBtn = 'button[aria-label="Search submit button"]';
|
const searchBtn = 'button[aria-label="Search submit button"]';
|
||||||
const searchTextInput = 'input[aria-label="Search text input"]';
|
const searchTextInput = 'input[aria-label="Search text input"]';
|
||||||
@@ -49,29 +49,12 @@ describe('<Search />', () => {
|
|||||||
expect(onSearch).toBeCalledWith('name__icontains', 'test-321');
|
expect(onSearch).toBeCalledWith('name__icontains', 'test-321');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleDropdownToggle properly updates state', async () => {
|
test('changing key select updates which key is called for onSearch', () => {
|
||||||
const columns = [{ name: 'Name', key: 'name', isDefault: true }];
|
const searchButton = 'button[aria-label="Search submit button"]';
|
||||||
const onSearch = jest.fn();
|
const searchTextInput = 'input[aria-label="Search text input"]';
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<Toolbar
|
|
||||||
id={`${QS_CONFIG.namespace}-list-toolbar`}
|
|
||||||
clearAllFilters={() => {}}
|
|
||||||
collapseListedFiltersBreakpoint="lg"
|
|
||||||
>
|
|
||||||
<ToolbarContent>
|
|
||||||
<Search qsConfig={QS_CONFIG} columns={columns} onSearch={onSearch} />
|
|
||||||
</ToolbarContent>
|
|
||||||
</Toolbar>
|
|
||||||
).find('Search');
|
|
||||||
expect(wrapper.state('isSearchDropdownOpen')).toEqual(false);
|
|
||||||
wrapper.instance().handleDropdownToggle(true);
|
|
||||||
expect(wrapper.state('isSearchDropdownOpen')).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handleDropdownSelect properly updates state', async () => {
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'Name', key: 'name', isDefault: true },
|
{ name: 'Name', key: 'name__icontains', isDefault: true },
|
||||||
{ name: 'Description', key: 'description' },
|
{ name: 'Description', key: 'description__icontains' },
|
||||||
];
|
];
|
||||||
const onSearch = jest.fn();
|
const onSearch = jest.fn();
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
@@ -84,18 +67,26 @@ describe('<Search />', () => {
|
|||||||
<Search qsConfig={QS_CONFIG} columns={columns} onSearch={onSearch} />
|
<Search qsConfig={QS_CONFIG} columns={columns} onSearch={onSearch} />
|
||||||
</ToolbarContent>
|
</ToolbarContent>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
).find('Search');
|
);
|
||||||
expect(wrapper.state('searchKey')).toEqual('name');
|
|
||||||
wrapper
|
act(() => {
|
||||||
.instance()
|
wrapper
|
||||||
.handleDropdownSelect({ target: { innerText: 'Description' } });
|
.find('Select[aria-label="Simple key select"]')
|
||||||
expect(wrapper.state('searchKey')).toEqual('description');
|
.invoke('onSelect')({ target: { innerText: 'Description' } });
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
wrapper.find(searchTextInput).instance().value = 'test-321';
|
||||||
|
wrapper.find(searchTextInput).simulate('change');
|
||||||
|
wrapper.find(searchButton).simulate('click');
|
||||||
|
|
||||||
|
expect(onSearch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onSearch).toBeCalledWith('description__icontains', 'test-321');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('attempt to search with empty string', () => {
|
test('attempt to search with empty string', () => {
|
||||||
const searchButton = 'button[aria-label="Search submit button"]';
|
const searchButton = 'button[aria-label="Search submit button"]';
|
||||||
const searchTextInput = 'input[aria-label="Search text input"]';
|
const searchTextInput = 'input[aria-label="Search text input"]';
|
||||||
const columns = [{ name: 'Name', key: 'name', isDefault: true }];
|
const columns = [{ name: 'Name', key: 'name__icontains', isDefault: true }];
|
||||||
const onSearch = jest.fn();
|
const onSearch = jest.fn();
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<Toolbar
|
<Toolbar
|
||||||
@@ -119,7 +110,7 @@ describe('<Search />', () => {
|
|||||||
test('search with a valid string', () => {
|
test('search with a valid string', () => {
|
||||||
const searchButton = 'button[aria-label="Search submit button"]';
|
const searchButton = 'button[aria-label="Search submit button"]';
|
||||||
const searchTextInput = 'input[aria-label="Search text input"]';
|
const searchTextInput = 'input[aria-label="Search text input"]';
|
||||||
const columns = [{ name: 'Name', key: 'name', isDefault: true }];
|
const columns = [{ name: 'Name', key: 'name__icontains', isDefault: true }];
|
||||||
const onSearch = jest.fn();
|
const onSearch = jest.fn();
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<Toolbar
|
<Toolbar
|
||||||
@@ -143,12 +134,12 @@ describe('<Search />', () => {
|
|||||||
|
|
||||||
test('filter keys are properly labeled', () => {
|
test('filter keys are properly labeled', () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'Name', key: 'name', isDefault: true },
|
{ name: 'Name', key: 'name__icontains', isDefault: true },
|
||||||
{ name: 'Type', key: 'type', options: [['foo', 'Foo Bar!']] },
|
{ name: 'Type', key: 'or__scm_type', options: [['foo', 'Foo Bar!']] },
|
||||||
{ name: 'Description', key: 'description' },
|
{ name: 'Description', key: 'description' },
|
||||||
];
|
];
|
||||||
const query =
|
const query =
|
||||||
'?organization.or__type=foo&organization.name=bar&item.page_size=10';
|
'?organization.or__scm_type=foo&organization.name__icontains=bar&item.page_size=10';
|
||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: [`/organizations/${query}`],
|
initialEntries: [`/organizations/${query}`],
|
||||||
});
|
});
|
||||||
@@ -165,13 +156,15 @@ describe('<Search />', () => {
|
|||||||
{ context: { router: { history } } }
|
{ context: { router: { history } } }
|
||||||
);
|
);
|
||||||
const typeFilterWrapper = wrapper.find(
|
const typeFilterWrapper = wrapper.find(
|
||||||
'ToolbarFilter[categoryName="Type"]'
|
'ToolbarFilter[categoryName="Type (or__scm_type)"]'
|
||||||
);
|
);
|
||||||
expect(typeFilterWrapper.prop('chips')[0].key).toEqual('or__type:foo');
|
expect(typeFilterWrapper.prop('chips')[0].key).toEqual('or__scm_type:foo');
|
||||||
const nameFilterWrapper = wrapper.find(
|
const nameFilterWrapper = wrapper.find(
|
||||||
'ToolbarFilter[categoryName="Name"]'
|
'ToolbarFilter[categoryName="Name (name__icontains)"]'
|
||||||
|
);
|
||||||
|
expect(nameFilterWrapper.prop('chips')[0].key).toEqual(
|
||||||
|
'name__icontains:bar'
|
||||||
);
|
);
|
||||||
expect(nameFilterWrapper.prop('chips')[0].key).toEqual('name:bar');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should test handle remove of option-based key', async () => {
|
test('should test handle remove of option-based key', async () => {
|
||||||
@@ -265,4 +258,37 @@ describe('<Search />', () => {
|
|||||||
});
|
});
|
||||||
expect(onRemove).toBeCalledWith('or__type', '');
|
expect(onRemove).toBeCalledWith('or__type', '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("ToolbarFilter added for any key that doesn't have search column", () => {
|
||||||
|
const columns = [
|
||||||
|
{ name: 'Name', key: 'name__icontains', isDefault: true },
|
||||||
|
{ name: 'Type', key: 'or__scm_type', options: [['foo', 'Foo Bar!']] },
|
||||||
|
{ name: 'Description', key: 'description' },
|
||||||
|
];
|
||||||
|
const query =
|
||||||
|
'?organization.or__scm_type=foo&organization.name__icontains=bar&organization.name__exact=baz&item.page_size=10&organization.foo=bar';
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: [`/organizations/${query}`],
|
||||||
|
});
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<Toolbar
|
||||||
|
id={`${QS_CONFIG.namespace}-list-toolbar`}
|
||||||
|
clearAllFilters={() => {}}
|
||||||
|
collapseListedFiltersBreakpoint="lg"
|
||||||
|
>
|
||||||
|
<ToolbarContent>
|
||||||
|
<Search qsConfig={QS_CONFIG} columns={columns} />
|
||||||
|
</ToolbarContent>
|
||||||
|
</Toolbar>,
|
||||||
|
{ context: { router: { history } } }
|
||||||
|
);
|
||||||
|
const nameExactFilterWrapper = wrapper.find(
|
||||||
|
'ToolbarFilter[categoryName="name__exact"]'
|
||||||
|
);
|
||||||
|
expect(nameExactFilterWrapper.prop('chips')[0].key).toEqual(
|
||||||
|
'name__exact:baz'
|
||||||
|
);
|
||||||
|
const fooFilterWrapper = wrapper.find('ToolbarFilter[categoryName="foo"]');
|
||||||
|
expect(fooFilterWrapper.prop('chips')[0].key).toEqual('foo:bar');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -102,9 +102,16 @@ class Sort extends React.Component {
|
|||||||
const { up } = DropdownPosition;
|
const { up } = DropdownPosition;
|
||||||
const { columns, i18n } = this.props;
|
const { columns, i18n } = this.props;
|
||||||
const { isSortDropdownOpen, sortKey, sortOrder, isNumeric } = this.state;
|
const { isSortDropdownOpen, sortKey, sortOrder, isNumeric } = this.state;
|
||||||
const [{ name: sortedColumnName }] = columns.filter(
|
|
||||||
({ key }) => key === sortKey
|
const defaultSortedColumn = columns.find(({ key }) => key === sortKey);
|
||||||
);
|
|
||||||
|
if (!defaultSortedColumn) {
|
||||||
|
throw new Error(
|
||||||
|
'sortKey must match one of the column keys, check the sortColumns prop passed to <Sort />'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedColumnName = defaultSortedColumn?.name;
|
||||||
|
|
||||||
const sortDropdownItems = columns
|
const sortDropdownItems = columns
|
||||||
.filter(({ key }) => key !== sortKey)
|
.filter(({ key }) => key !== sortKey)
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ describe('<UserAndTeamAccessAdd/>', () => {
|
|||||||
fetchItems: JobTemplatesAPI.read,
|
fetchItems: JobTemplatesAPI.read,
|
||||||
label: 'Job template',
|
label: 'Job template',
|
||||||
selectedResource: 'jobTemplate',
|
selectedResource: 'jobTemplate',
|
||||||
searchColumns: [{ name: 'Name', key: 'name', isDefault: true }],
|
searchColumns: [
|
||||||
|
{ name: 'Name', key: 'name__icontains', isDefault: true },
|
||||||
|
],
|
||||||
sortColumns: [{ name: 'Name', key: 'name' }],
|
sortColumns: [{ name: 'Name', key: 'name' }],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -116,7 +118,9 @@ describe('<UserAndTeamAccessAdd/>', () => {
|
|||||||
fetchItems: JobTemplatesAPI.read,
|
fetchItems: JobTemplatesAPI.read,
|
||||||
label: 'Job template',
|
label: 'Job template',
|
||||||
selectedResource: 'jobTemplate',
|
selectedResource: 'jobTemplate',
|
||||||
searchColumns: [{ name: 'Name', key: 'name', isDefault: true }],
|
searchColumns: [
|
||||||
|
{ name: 'Name', key: 'name__icontains', isDefault: true },
|
||||||
|
],
|
||||||
sortColumns: [{ name: 'Name', key: 'name' }],
|
sortColumns: [{ name: 'Name', key: 'name' }],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -190,7 +194,9 @@ describe('<UserAndTeamAccessAdd/>', () => {
|
|||||||
fetchItems: JobTemplatesAPI.read,
|
fetchItems: JobTemplatesAPI.read,
|
||||||
label: 'Job template',
|
label: 'Job template',
|
||||||
selectedResource: 'jobTemplate',
|
selectedResource: 'jobTemplate',
|
||||||
searchColumns: [{ name: 'Name', key: 'name', isDefault: true }],
|
searchColumns: [
|
||||||
|
{ name: 'Name', key: 'name__icontains', isDefault: true },
|
||||||
|
],
|
||||||
sortColumns: [{ name: 'Name', key: 'name' }],
|
sortColumns: [{ name: 'Name', key: 'name' }],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,20 +16,20 @@ export default function getResourceAccessConfig(i18n) {
|
|||||||
searchColumns: [
|
searchColumns: [
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Playbook name`),
|
name: i18n._(t`Playbook name`),
|
||||||
key: 'playbook',
|
key: 'playbook__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sortColumns: [
|
sortColumns: [
|
||||||
@@ -46,20 +46,20 @@ export default function getResourceAccessConfig(i18n) {
|
|||||||
searchColumns: [
|
searchColumns: [
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Playbook name`),
|
name: i18n._(t`Playbook name`),
|
||||||
key: 'playbook',
|
key: 'playbook__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sortColumns: [
|
sortColumns: [
|
||||||
@@ -76,12 +76,12 @@ export default function getResourceAccessConfig(i18n) {
|
|||||||
searchColumns: [
|
searchColumns: [
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Type`),
|
name: i18n._(t`Type`),
|
||||||
key: 'scm_type',
|
key: 'or__scm_type',
|
||||||
options: [
|
options: [
|
||||||
[``, i18n._(t`Manual`)],
|
[``, i18n._(t`Manual`)],
|
||||||
[`git`, i18n._(t`Git`)],
|
[`git`, i18n._(t`Git`)],
|
||||||
@@ -92,15 +92,15 @@ export default function getResourceAccessConfig(i18n) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Source Control URL`),
|
name: i18n._(t`Source Control URL`),
|
||||||
key: 'scm_url',
|
key: 'scm_url__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sortColumns: [
|
sortColumns: [
|
||||||
@@ -117,16 +117,16 @@ export default function getResourceAccessConfig(i18n) {
|
|||||||
searchColumns: [
|
searchColumns: [
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sortColumns: [
|
sortColumns: [
|
||||||
@@ -143,12 +143,12 @@ export default function getResourceAccessConfig(i18n) {
|
|||||||
searchColumns: [
|
searchColumns: [
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Type`),
|
name: i18n._(t`Type`),
|
||||||
key: 'scm_type',
|
key: 'or__scm_type',
|
||||||
options: [
|
options: [
|
||||||
[``, i18n._(t`Manual`)],
|
[``, i18n._(t`Manual`)],
|
||||||
[`git`, i18n._(t`Git`)],
|
[`git`, i18n._(t`Git`)],
|
||||||
@@ -159,15 +159,15 @@ export default function getResourceAccessConfig(i18n) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Source Control URL`),
|
name: i18n._(t`Source Control URL`),
|
||||||
key: 'scm_url',
|
key: 'scm_url__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sortColumns: [
|
sortColumns: [
|
||||||
@@ -184,16 +184,16 @@ export default function getResourceAccessConfig(i18n) {
|
|||||||
searchColumns: [
|
searchColumns: [
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sortColumns: [
|
sortColumns: [
|
||||||
|
|||||||
@@ -26,14 +26,20 @@ function ApplicationTokenList({ i18n }) {
|
|||||||
const {
|
const {
|
||||||
error,
|
error,
|
||||||
isLoading,
|
isLoading,
|
||||||
result: { tokens, itemCount },
|
result: { tokens, itemCount, actions, relatedSearchFields },
|
||||||
request: fetchTokens,
|
request: fetchTokens,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
const {
|
const [
|
||||||
data: { results, count },
|
{
|
||||||
} = await ApplicationsAPI.readTokens(id, params);
|
data: { results, count },
|
||||||
|
},
|
||||||
|
actionsResponse,
|
||||||
|
] = await Promise.all([
|
||||||
|
ApplicationsAPI.readTokens(id, params),
|
||||||
|
ApplicationsAPI.readTokenOptions(id),
|
||||||
|
]);
|
||||||
const modifiedResults = results.map(result => {
|
const modifiedResults = results.map(result => {
|
||||||
result.summary_fields = {
|
result.summary_fields = {
|
||||||
user: result.summary_fields.user,
|
user: result.summary_fields.user,
|
||||||
@@ -43,9 +49,16 @@ function ApplicationTokenList({ i18n }) {
|
|||||||
result.name = result.summary_fields.user?.username;
|
result.name = result.summary_fields.user?.username;
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
return { tokens: modifiedResults, itemCount: count };
|
return {
|
||||||
|
tokens: modifiedResults,
|
||||||
|
itemCount: count,
|
||||||
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
|
};
|
||||||
}, [id, location.search]),
|
}, [id, location.search]),
|
||||||
{ tokens: [], itemCount: 0 }
|
{ tokens: [], itemCount: 0, actions: {}, relatedSearchFields: [] }
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -77,6 +90,12 @@ function ApplicationTokenList({ i18n }) {
|
|||||||
await handleDeleteApplications();
|
await handleDeleteApplications();
|
||||||
setSelected([]);
|
setSelected([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
@@ -90,7 +109,7 @@ function ApplicationTokenList({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'user__username',
|
key: 'user__username__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@@ -116,6 +135,8 @@ function ApplicationTokenList({ i18n }) {
|
|||||||
key: 'modified',
|
key: 'modified',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderToolbar={props => (
|
renderToolbar={props => (
|
||||||
<DatalistToolbar
|
<DatalistToolbar
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -82,6 +82,19 @@ const tokens = {
|
|||||||
};
|
};
|
||||||
describe('<ApplicationTokenList/>', () => {
|
describe('<ApplicationTokenList/>', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ApplicationsAPI.readTokenOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {},
|
||||||
|
POST: {},
|
||||||
|
},
|
||||||
|
related_search_fields: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('should mount properly', async () => {
|
test('should mount properly', async () => {
|
||||||
ApplicationsAPI.readTokens.mockResolvedValue(tokens);
|
ApplicationsAPI.readTokens.mockResolvedValue(tokens);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function ApplicationsList({ i18n }) {
|
|||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
request: fetchApplications,
|
request: fetchApplications,
|
||||||
result: { applications, itemCount, actions },
|
result: { applications, itemCount, actions, relatedSearchFields },
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
@@ -46,12 +46,16 @@ function ApplicationsList({ i18n }) {
|
|||||||
applications: response.data.results,
|
applications: response.data.results,
|
||||||
itemCount: response.data.count,
|
itemCount: response.data.count,
|
||||||
actions: actionsResponse.data.actions,
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [location]),
|
}, [location]),
|
||||||
{
|
{
|
||||||
applications: [],
|
applications: [],
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
actions: {},
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -85,6 +89,10 @@ function ApplicationsList({ i18n }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const canAdd = actions && actions.POST;
|
const canAdd = actions && actions.POST;
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -101,12 +109,12 @@ function ApplicationsList({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Description`),
|
name: i18n._(t`Description`),
|
||||||
key: 'description',
|
key: 'description__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
@@ -127,6 +135,8 @@ function ApplicationsList({ i18n }) {
|
|||||||
key: 'description',
|
key: 'description',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderToolbar={props => (
|
renderToolbar={props => (
|
||||||
<DatalistToolbar
|
<DatalistToolbar
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ CredentialsAPI.read.mockResolvedValue({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
CredentialsAPI.readOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {},
|
||||||
|
POST: {},
|
||||||
|
},
|
||||||
|
related_search_fields: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
CredentialTypesAPI.readDetail.mockResolvedValue({
|
CredentialTypesAPI.readDetail.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
id: 20,
|
id: 20,
|
||||||
|
|||||||
@@ -25,28 +25,38 @@ function CredentialsStep({ i18n }) {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { credentials, count },
|
result: { credentials, count, actions, relatedSearchFields },
|
||||||
error: credentialsError,
|
error: credentialsError,
|
||||||
isLoading: isCredentialsLoading,
|
isLoading: isCredentialsLoading,
|
||||||
request: fetchCredentials,
|
request: fetchCredentials,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||||
const { data } = await CredentialsAPI.read({
|
const [{ data }, actionsResponse] = await Promise.all([
|
||||||
...params,
|
CredentialsAPI.read({ ...params }),
|
||||||
});
|
CredentialsAPI.readOptions(),
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
credentials: data.results,
|
credentials: data.results,
|
||||||
count: data.count,
|
count: data.count,
|
||||||
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [history.location.search]),
|
}, [history.location.search]),
|
||||||
{ credentials: [], count: 0 }
|
{ credentials: [], count: 0, actions: {}, relatedSearchFields: [] }
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCredentials();
|
fetchCredentials();
|
||||||
}, [fetchCredentials]);
|
}, [fetchCredentials]);
|
||||||
|
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
if (credentialsError) {
|
if (credentialsError) {
|
||||||
return <ContentError error={credentialsError} />;
|
return <ContentError error={credentialsError} />;
|
||||||
}
|
}
|
||||||
@@ -76,16 +86,16 @@ function CredentialsStep({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
@@ -94,6 +104,8 @@ function CredentialsStep({ i18n }) {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function HostGroupsList({ i18n, host }) {
|
|||||||
const invId = host.summary_fields.inventory.id;
|
const invId = host.summary_fields.inventory.id;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { groups, itemCount, actions },
|
result: { groups, itemCount, actions, relatedSearchFields },
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchGroups,
|
request: fetchGroups,
|
||||||
@@ -55,11 +55,16 @@ function HostGroupsList({ i18n, host }) {
|
|||||||
groups: results,
|
groups: results,
|
||||||
itemCount: count,
|
itemCount: count,
|
||||||
actions: actionsResponse.data.actions,
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [hostId, search]),
|
}, [hostId, search]),
|
||||||
{
|
{
|
||||||
groups: [],
|
groups: [],
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -123,6 +128,10 @@ function HostGroupsList({ i18n, host }) {
|
|||||||
|
|
||||||
const canAdd =
|
const canAdd =
|
||||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -136,16 +145,16 @@ function HostGroupsList({ i18n, host }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
@@ -154,6 +163,8 @@ function HostGroupsList({ i18n, host }) {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderItem={item => (
|
renderItem={item => (
|
||||||
<HostGroupItem
|
<HostGroupItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function HostList({ i18n }) {
|
|||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { hosts, count, actions },
|
result: { hosts, count, actions, relatedSearchFields },
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchHosts,
|
request: fetchHosts,
|
||||||
@@ -44,12 +44,16 @@ function HostList({ i18n }) {
|
|||||||
hosts: results[0].data.results,
|
hosts: results[0].data.results,
|
||||||
count: results[0].data.count,
|
count: results[0].data.count,
|
||||||
actions: results[1].data.actions,
|
actions: results[1].data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
results[1]?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [location]),
|
}, [location]),
|
||||||
{
|
{
|
||||||
hosts: [],
|
hosts: [],
|
||||||
count: 0,
|
count: 0,
|
||||||
actions: {},
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -93,6 +97,10 @@ function HostList({ i18n }) {
|
|||||||
|
|
||||||
const canAdd =
|
const canAdd =
|
||||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
@@ -108,16 +116,16 @@ function HostList({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
@@ -126,6 +134,8 @@ function HostList({ i18n }) {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderToolbar={props => (
|
renderToolbar={props => (
|
||||||
<DataListToolbar
|
<DataListToolbar
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { hosts, hostCount, actions },
|
result: { hosts, hostCount, actions, relatedSearchFields },
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchHosts,
|
request: fetchHosts,
|
||||||
@@ -48,11 +48,16 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
hosts: response.data.results,
|
hosts: response.data.results,
|
||||||
hostCount: response.data.count,
|
hostCount: response.data.count,
|
||||||
actions: actionsResponse.data.actions,
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [groupId, inventoryId, location.search]),
|
}, [groupId, inventoryId, location.search]),
|
||||||
{
|
{
|
||||||
hosts: [],
|
hosts: [],
|
||||||
hostCount: 0,
|
hostCount: 0,
|
||||||
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -122,6 +127,10 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
const canAdd =
|
const canAdd =
|
||||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
const addFormUrl = `/inventories/inventory/${inventoryId}/groups/${groupId}/nested_hosts/add`;
|
const addFormUrl = `/inventories/inventory/${inventoryId}/groups/${groupId}/nested_hosts/add`;
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -136,16 +145,16 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
@@ -154,6 +163,8 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderToolbar={props => (
|
renderToolbar={props => (
|
||||||
<DataListToolbar
|
<DataListToolbar
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -154,12 +154,12 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created by (username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified by (username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { groups, itemCount, actions },
|
result: { groups, itemCount, actions, relatedSearchFields },
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchGroups,
|
request: fetchGroups,
|
||||||
@@ -53,11 +53,16 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
groups: results,
|
groups: results,
|
||||||
itemCount: count,
|
itemCount: count,
|
||||||
actions: actionsResponse.data.actions,
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [hostId, search]), // eslint-disable-line react-hooks/exhaustive-deps
|
}, [hostId, search]), // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
{
|
{
|
||||||
groups: [],
|
groups: [],
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -121,6 +126,10 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
|
|
||||||
const canAdd =
|
const canAdd =
|
||||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -134,16 +143,16 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
@@ -152,6 +161,8 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderItem={item => (
|
renderItem={item => (
|
||||||
<InventoryHostGroupItem
|
<InventoryHostGroupItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function InventoryList({ i18n }) {
|
|||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { results, itemCount, actions },
|
result: { results, itemCount, actions, relatedSearchFields },
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchInventories,
|
request: fetchInventories,
|
||||||
@@ -44,12 +44,16 @@ function InventoryList({ i18n }) {
|
|||||||
results: response.data.results,
|
results: response.data.results,
|
||||||
itemCount: response.data.count,
|
itemCount: response.data.count,
|
||||||
actions: actionsResponse.data.actions,
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [location]),
|
}, [location]),
|
||||||
{
|
{
|
||||||
results: [],
|
results: [],
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
actions: {},
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -93,6 +97,10 @@ function InventoryList({ i18n }) {
|
|||||||
|
|
||||||
const hasContentLoading = isDeleteLoading || isLoading;
|
const hasContentLoading = isDeleteLoading || isLoading;
|
||||||
const canAdd = actions && actions.POST;
|
const canAdd = actions && actions.POST;
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelectAll = isSelected => {
|
const handleSelectAll = isSelected => {
|
||||||
setSelected(isSelected ? [...inventories] : []);
|
setSelected(isSelected ? [...inventories] : []);
|
||||||
@@ -135,16 +143,16 @@ function InventoryList({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
@@ -153,6 +161,8 @@ function InventoryList({ i18n }) {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderToolbar={props => (
|
renderToolbar={props => (
|
||||||
<DatalistToolbar
|
<DatalistToolbar
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function OrganizationsList({ i18n }) {
|
|||||||
const addUrl = `${match.url}/add`;
|
const addUrl = `${match.url}/add`;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { organizations, organizationCount, actions },
|
result: { organizations, organizationCount, actions, relatedSearchFields },
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading: isOrgsLoading,
|
isLoading: isOrgsLoading,
|
||||||
request: fetchOrganizations,
|
request: fetchOrganizations,
|
||||||
@@ -46,12 +46,16 @@ function OrganizationsList({ i18n }) {
|
|||||||
organizations: orgs.data.results,
|
organizations: orgs.data.results,
|
||||||
organizationCount: orgs.data.count,
|
organizationCount: orgs.data.count,
|
||||||
actions: orgActions.data.actions,
|
actions: orgActions.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
orgActions?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [location]),
|
}, [location]),
|
||||||
{
|
{
|
||||||
organizations: [],
|
organizations: [],
|
||||||
organizationCount: 0,
|
organizationCount: 0,
|
||||||
actions: {},
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -86,6 +90,10 @@ function OrganizationsList({ i18n }) {
|
|||||||
|
|
||||||
const hasContentLoading = isDeleteLoading || isOrgsLoading;
|
const hasContentLoading = isDeleteLoading || isOrgsLoading;
|
||||||
const canAdd = actions && actions.POST;
|
const canAdd = actions && actions.POST;
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelectAll = isSelected => {
|
const handleSelectAll = isSelected => {
|
||||||
setSelected(isSelected ? [...organizations] : []);
|
setSelected(isSelected ? [...organizations] : []);
|
||||||
@@ -114,16 +122,16 @@ function OrganizationsList({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
@@ -132,6 +140,8 @@ function OrganizationsList({ i18n }) {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderToolbar={props => (
|
renderToolbar={props => (
|
||||||
<DataListToolbar
|
<DataListToolbar
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -53,16 +53,16 @@ function OrganizationTeamList({ id, i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created by (username)`),
|
name: i18n._(t`Created by (username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified by (username)`),
|
name: i18n._(t`Modified by (username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
|
|||||||
@@ -104,20 +104,16 @@ function ProjectJobTemplatesList({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Playbook name`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'job_template__playbook',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created by (username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n._(t`Modified by (username)`),
|
|
||||||
key: 'modified_by__username',
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function ProjectList({ i18n }) {
|
|||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { results, itemCount, actions },
|
result: { results, itemCount, actions, relatedSearchFields },
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchProjects,
|
request: fetchProjects,
|
||||||
@@ -45,12 +45,16 @@ function ProjectList({ i18n }) {
|
|||||||
results: response.data.results,
|
results: response.data.results,
|
||||||
itemCount: response.data.count,
|
itemCount: response.data.count,
|
||||||
actions: actionsResponse.data.actions,
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [location]),
|
}, [location]),
|
||||||
{
|
{
|
||||||
results: [],
|
results: [],
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
actions: {},
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -85,6 +89,10 @@ function ProjectList({ i18n }) {
|
|||||||
|
|
||||||
const hasContentLoading = isDeleteLoading || isLoading;
|
const hasContentLoading = isDeleteLoading || isLoading;
|
||||||
const canAdd = actions && actions.POST;
|
const canAdd = actions && actions.POST;
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelectAll = isSelected => {
|
const handleSelectAll = isSelected => {
|
||||||
setSelected(isSelected ? [...projects] : []);
|
setSelected(isSelected ? [...projects] : []);
|
||||||
@@ -113,12 +121,12 @@ function ProjectList({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Type`),
|
name: i18n._(t`Type`),
|
||||||
key: 'scm_type',
|
key: 'or__scm_type',
|
||||||
options: [
|
options: [
|
||||||
[``, i18n._(t`Manual`)],
|
[``, i18n._(t`Manual`)],
|
||||||
[`git`, i18n._(t`Git`)],
|
[`git`, i18n._(t`Git`)],
|
||||||
@@ -129,17 +137,19 @@ function ProjectList({ i18n }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Source Control URL`),
|
name: i18n._(t`Source Control URL`),
|
||||||
key: 'scm_url',
|
key: 'scm_url__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ describe('<ProjectList />', () => {
|
|||||||
GET: {},
|
GET: {},
|
||||||
POST: {},
|
POST: {},
|
||||||
},
|
},
|
||||||
|
related_search_fields: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function TeamList({ i18n }) {
|
|||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { teams, itemCount, actions },
|
result: { teams, itemCount, actions, relatedSearchFields },
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchTeams,
|
request: fetchTeams,
|
||||||
@@ -44,12 +44,16 @@ function TeamList({ i18n }) {
|
|||||||
teams: response.data.results,
|
teams: response.data.results,
|
||||||
itemCount: response.data.count,
|
itemCount: response.data.count,
|
||||||
actions: actionsResponse.data.actions,
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [location]),
|
}, [location]),
|
||||||
{
|
{
|
||||||
teams: [],
|
teams: [],
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
actions: {},
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -81,6 +85,10 @@ function TeamList({ i18n }) {
|
|||||||
|
|
||||||
const hasContentLoading = isDeleteLoading || isLoading;
|
const hasContentLoading = isDeleteLoading || isLoading;
|
||||||
const canAdd = actions && actions.POST;
|
const canAdd = actions && actions.POST;
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelectAll = isSelected => {
|
const handleSelectAll = isSelected => {
|
||||||
setSelected(isSelected ? [...teams] : []);
|
setSelected(isSelected ? [...teams] : []);
|
||||||
@@ -109,20 +117,20 @@ function TeamList({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Organization Name`),
|
name: i18n._(t`Organization Name`),
|
||||||
key: 'organization__name',
|
key: 'organization__name__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
@@ -131,6 +139,8 @@ function TeamList({ i18n }) {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderToolbar={props => (
|
renderToolbar={props => (
|
||||||
<DataListToolbar
|
<DataListToolbar
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function TeamRolesList({ i18n, me, team }) {
|
|||||||
isLoading,
|
isLoading,
|
||||||
request: fetchRoles,
|
request: fetchRoles,
|
||||||
contentError,
|
contentError,
|
||||||
result: { roleCount, roles, isAdminOfOrg },
|
result: { roleCount, roles, isAdminOfOrg, actions, relatedSearchFields },
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, search);
|
const params = parseQueryString(QS_CONFIG, search);
|
||||||
@@ -46,22 +46,30 @@ function TeamRolesList({ i18n, me, team }) {
|
|||||||
data: { results, count },
|
data: { results, count },
|
||||||
},
|
},
|
||||||
{ count: orgAdminCount },
|
{ count: orgAdminCount },
|
||||||
|
actionsResponse,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
TeamsAPI.readRoles(team.id, params),
|
TeamsAPI.readRoles(team.id, params),
|
||||||
UsersAPI.readAdminOfOrganizations(me.id, {
|
UsersAPI.readAdminOfOrganizations(me.id, {
|
||||||
id: team.organization,
|
id: team.organization,
|
||||||
}),
|
}),
|
||||||
|
TeamsAPI.readRoleOptions(team.id),
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
roleCount: count,
|
roleCount: count,
|
||||||
roles: results,
|
roles: results,
|
||||||
isAdminOfOrg: orgAdminCount > 0,
|
isAdminOfOrg: orgAdminCount > 0,
|
||||||
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [me.id, team.id, team.organization, search]),
|
}, [me.id, team.id, team.organization, search]),
|
||||||
{
|
{
|
||||||
roles: [],
|
roles: [],
|
||||||
roleCount: 0,
|
roleCount: 0,
|
||||||
isAdminOfOrg: false,
|
isAdminOfOrg: false,
|
||||||
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -90,6 +98,10 @@ function TeamRolesList({ i18n, me, team }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const canAdd = team?.summary_fields?.user_capabilities?.edit || isAdminOfOrg;
|
const canAdd = team?.summary_fields?.user_capabilities?.edit || isAdminOfOrg;
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
const detailUrl = role => {
|
const detailUrl = role => {
|
||||||
const { resource_id, resource_type } = role.summary_fields;
|
const { resource_id, resource_type } = role.summary_fields;
|
||||||
@@ -136,16 +148,18 @@ function TeamRolesList({ i18n, me, team }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Role`),
|
name: i18n._(t`Role`),
|
||||||
key: 'role_field',
|
key: 'role_field__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`ID`),
|
||||||
key: 'id',
|
key: 'id',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderToolbar={props => (
|
renderToolbar={props => (
|
||||||
<DataListToolbar
|
<DataListToolbar
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -164,6 +164,13 @@ describe('<TeamRolesList />', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
TeamsAPI.readRoleOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: { GET: {} },
|
||||||
|
related_search_fields: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function TemplateList({ i18n }) {
|
|||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { results, count, jtActions, wfjtActions },
|
result: { results, count, jtActions, wfjtActions, relatedSearchFields },
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchTemplates,
|
request: fetchTemplates,
|
||||||
@@ -53,6 +53,14 @@ function TemplateList({ i18n }) {
|
|||||||
count: responses[0].data.count,
|
count: responses[0].data.count,
|
||||||
jtActions: responses[1].data.actions,
|
jtActions: responses[1].data.actions,
|
||||||
wfjtActions: responses[2].data.actions,
|
wfjtActions: responses[2].data.actions,
|
||||||
|
relatedSearchFields: [
|
||||||
|
...(responses[1]?.data?.related_search_fields || []).map(val =>
|
||||||
|
val.slice(0, -8)
|
||||||
|
),
|
||||||
|
...(responses[2]?.data?.related_search_fields || []).map(val =>
|
||||||
|
val.slice(0, -8)
|
||||||
|
),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}, [location]),
|
}, [location]),
|
||||||
{
|
{
|
||||||
@@ -60,6 +68,7 @@ function TemplateList({ i18n }) {
|
|||||||
count: 0,
|
count: 0,
|
||||||
jtActions: {},
|
jtActions: {},
|
||||||
wfjtActions: {},
|
wfjtActions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -118,6 +127,18 @@ function TemplateList({ i18n }) {
|
|||||||
jtActions && Object.prototype.hasOwnProperty.call(jtActions, 'POST');
|
jtActions && Object.prototype.hasOwnProperty.call(jtActions, 'POST');
|
||||||
const canAddWFJT =
|
const canAddWFJT =
|
||||||
wfjtActions && Object.prototype.hasOwnProperty.call(wfjtActions, 'POST');
|
wfjtActions && Object.prototype.hasOwnProperty.call(wfjtActions, 'POST');
|
||||||
|
// spreading Set() returns only unique keys
|
||||||
|
const relatedSearchableKeys = [...new Set(relatedSearchFields)] || [];
|
||||||
|
const searchableKeys = [
|
||||||
|
...new Set([
|
||||||
|
...Object.keys(jtActions?.GET || {}).filter(
|
||||||
|
key => jtActions.GET[key].filterable
|
||||||
|
),
|
||||||
|
...Object.keys(wfjtActions?.GET || {}).filter(
|
||||||
|
key => wfjtActions.GET[key].filterable
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
];
|
||||||
const addButtonOptions = [];
|
const addButtonOptions = [];
|
||||||
|
|
||||||
if (canAddJT) {
|
if (canAddJT) {
|
||||||
@@ -152,16 +173,16 @@ function TemplateList({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Description`),
|
name: i18n._(t`Description`),
|
||||||
key: 'description',
|
key: 'description__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Type`),
|
name: i18n._(t`Type`),
|
||||||
key: 'type',
|
key: 'or__type',
|
||||||
options: [
|
options: [
|
||||||
[`job_template`, i18n._(t`Job Template`)],
|
[`job_template`, i18n._(t`Job Template`)],
|
||||||
[`workflow_job_template`, i18n._(t`Workflow Template`)],
|
[`workflow_job_template`, i18n._(t`Workflow Template`)],
|
||||||
@@ -169,15 +190,15 @@ function TemplateList({ i18n }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Playbook name`),
|
name: i18n._(t`Playbook name`),
|
||||||
key: 'job_template__playbook',
|
key: 'job_template__playbook__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
@@ -206,6 +227,8 @@ function TemplateList({ i18n }) {
|
|||||||
key: 'type',
|
key: 'type',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderToolbar={props => (
|
renderToolbar={props => (
|
||||||
<DatalistToolbar
|
<DatalistToolbar
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -68,12 +68,12 @@ function InventorySourcesList({ i18n, nodeResource, onUpdateNodeResource }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Source`),
|
name: i18n._(t`Source`),
|
||||||
key: 'source',
|
key: 'or__source',
|
||||||
options: [
|
options: [
|
||||||
[`file`, i18n._(t`File, directory or script`)],
|
[`file`, i18n._(t`File, directory or script`)],
|
||||||
[`scm`, i18n._(t`Sourced from a project`)],
|
[`scm`, i18n._(t`Sourced from a project`)],
|
||||||
|
|||||||
@@ -70,20 +70,20 @@ function JobTemplatesList({ i18n, nodeResource, onUpdateNodeResource }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Playbook name`),
|
name: i18n._(t`Playbook name`),
|
||||||
key: 'playbook',
|
key: 'playbook__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created by (username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified by (username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
|
|||||||
@@ -68,12 +68,12 @@ function ProjectsList({ i18n, nodeResource, onUpdateNodeResource }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Type`),
|
name: i18n._(t`Type`),
|
||||||
key: 'scm_type',
|
key: 'or__scm_type',
|
||||||
options: [
|
options: [
|
||||||
[``, i18n._(t`Manual`)],
|
[``, i18n._(t`Manual`)],
|
||||||
[`git`, i18n._(t`Git`)],
|
[`git`, i18n._(t`Git`)],
|
||||||
@@ -83,16 +83,16 @@ function ProjectsList({ i18n, nodeResource, onUpdateNodeResource }) {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Source control URL`),
|
name: i18n._(t`Source Control URL`),
|
||||||
key: 'scm_url',
|
key: 'scm_url__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified by (username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created by (username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
|
|||||||
@@ -74,24 +74,24 @@ function WorkflowJobTemplatesList({
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Organization (name)`),
|
name: i18n._(t`Organization (Name)`),
|
||||||
key: 'organization__name',
|
key: 'organization__name__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Inventory (name)`),
|
name: i18n._(t`Inventory (Name)`),
|
||||||
key: 'inventory__name',
|
key: 'inventory__name__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created by (username)`),
|
name: i18n._(t`Created By (Username)`),
|
||||||
key: 'created_by__username',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified by (username)`),
|
name: i18n._(t`Modified By (Username)`),
|
||||||
key: 'modified_by__username',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ const QS_CONFIG = getQSConfig('roles', {
|
|||||||
function UserAccessList({ i18n, user }) {
|
function UserAccessList({ i18n, user }) {
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||||
|
|
||||||
const [roleToDisassociate, setRoleToDisassociate] = useState(null);
|
const [roleToDisassociate, setRoleToDisassociate] = useState(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchRoles,
|
request: fetchRoles,
|
||||||
error,
|
error,
|
||||||
result: { roleCount, roles, options },
|
result: { roleCount, roles, actions, relatedSearchFields },
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, search);
|
const params = parseQueryString(QS_CONFIG, search);
|
||||||
@@ -46,20 +46,28 @@ function UserAccessList({ i18n, user }) {
|
|||||||
{
|
{
|
||||||
data: { results, count },
|
data: { results, count },
|
||||||
},
|
},
|
||||||
{
|
actionsResponse,
|
||||||
data: { actions },
|
|
||||||
},
|
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
UsersAPI.readRoles(user.id, params),
|
UsersAPI.readRoles(user.id, params),
|
||||||
UsersAPI.readOptions(),
|
UsersAPI.readOptions(),
|
||||||
]);
|
]);
|
||||||
return { roleCount: count, roles: results, options: actions };
|
return {
|
||||||
|
roleCount: count,
|
||||||
|
roles: results,
|
||||||
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
|
};
|
||||||
}, [user.id, search]),
|
}, [user.id, search]),
|
||||||
{
|
{
|
||||||
roles: [],
|
roles: [],
|
||||||
roleCount: 0,
|
roleCount: 0,
|
||||||
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRoles();
|
fetchRoles();
|
||||||
}, [fetchRoles]);
|
}, [fetchRoles]);
|
||||||
@@ -82,7 +90,12 @@ function UserAccessList({ i18n, user }) {
|
|||||||
|
|
||||||
const canAdd =
|
const canAdd =
|
||||||
user?.summary_fields?.user_capabilities?.edit ||
|
user?.summary_fields?.user_capabilities?.edit ||
|
||||||
(options && Object.prototype.hasOwnProperty.call(options, 'POST'));
|
(actions && Object.prototype.hasOwnProperty.call(actions, 'POST'));
|
||||||
|
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
const saveRoles = () => {
|
const saveRoles = () => {
|
||||||
setIsWizardOpen(false);
|
setIsWizardOpen(false);
|
||||||
@@ -132,16 +145,18 @@ function UserAccessList({ i18n, user }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Role`),
|
name: i18n._(t`Role`),
|
||||||
key: 'role_field',
|
key: 'role_field__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`ID`),
|
||||||
key: 'id',
|
key: 'id',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderItem={role => {
|
renderItem={role => {
|
||||||
return (
|
return (
|
||||||
<UserAccessListItem
|
<UserAccessListItem
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ jest.mock('../../../api/models/Roles');
|
|||||||
|
|
||||||
UsersAPI.readOptions.mockResolvedValue({
|
UsersAPI.readOptions.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
actions: {
|
actions: { GET: {} },
|
||||||
GET: {},
|
related_search_fields: [],
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -144,6 +143,15 @@ describe('<UserAccessList />', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
test('should not render add button when user cannot create other users and user cannot edit this user', async () => {
|
test('should not render add button when user cannot create other users and user cannot edit this user', async () => {
|
||||||
|
UsersAPI.readRoleOptions.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {},
|
||||||
|
},
|
||||||
|
related_search_fields: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
UsersAPI.readRoles.mockResolvedValue({
|
UsersAPI.readRoles.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
results: [
|
results: [
|
||||||
|
|||||||
@@ -98,16 +98,16 @@ function UserList({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Username`),
|
name: i18n._(t`Username`),
|
||||||
key: 'username',
|
key: 'username__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`First name`),
|
name: i18n._(t`First Name`),
|
||||||
key: 'first_name',
|
key: 'first_name__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Last name`),
|
name: i18n._(t`Last Name`),
|
||||||
key: 'last_name',
|
key: 'last_name__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
@@ -116,11 +116,11 @@ function UserList({ i18n }) {
|
|||||||
key: 'username',
|
key: 'username',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`First name`),
|
name: i18n._(t`First Name`),
|
||||||
key: 'first_name',
|
key: 'first_name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Last name`),
|
name: i18n._(t`Last Name`),
|
||||||
key: 'last_name',
|
key: 'last_name',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -20,24 +20,36 @@ function UserTeamList({ i18n }) {
|
|||||||
const { id: userId } = useParams();
|
const { id: userId } = useParams();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { teams, count },
|
result: { teams, count, actions, relatedSearchFields },
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchOrgs,
|
request: fetchOrgs,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
const {
|
const [
|
||||||
data: { results, count: teamCount },
|
{
|
||||||
} = await UsersAPI.readTeams(userId, params);
|
data: { results, count: teamCount },
|
||||||
|
},
|
||||||
|
actionsResponse,
|
||||||
|
] = await Promise.all([
|
||||||
|
UsersAPI.readTeams(userId, params),
|
||||||
|
UsersAPI.readTeamsOptions(userId),
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
teams: results,
|
teams: results,
|
||||||
count: teamCount,
|
count: teamCount,
|
||||||
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
};
|
};
|
||||||
}, [userId, location.search]),
|
}, [userId, location.search]),
|
||||||
{
|
{
|
||||||
teams: [],
|
teams: [],
|
||||||
count: 0,
|
count: 0,
|
||||||
|
actions: {},
|
||||||
|
relatedSearchFields: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -45,6 +57,11 @@ function UserTeamList({ i18n }) {
|
|||||||
fetchOrgs();
|
fetchOrgs();
|
||||||
}, [fetchOrgs]);
|
}, [fetchOrgs]);
|
||||||
|
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
items={teams}
|
items={teams}
|
||||||
@@ -66,14 +83,16 @@ function UserTeamList({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'name',
|
key: 'name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Organization`),
|
name: i18n._(t`Organization`),
|
||||||
key: 'organization__name',
|
key: 'organization__name__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,13 +58,14 @@ describe('<UserTeamList />', () => {
|
|||||||
data: mockAPIUserTeamList.data,
|
data: mockAPIUserTeamList.data,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
UsersAPI.readOptions = jest.fn(() =>
|
UsersAPI.readTeamsOptions = jest.fn(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
data: {
|
data: {
|
||||||
actions: {
|
actions: {
|
||||||
GET: {},
|
GET: {},
|
||||||
POST: {},
|
POST: {},
|
||||||
},
|
},
|
||||||
|
related_search_fields: [],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,13 +28,19 @@ function UserTokenList({ i18n }) {
|
|||||||
error,
|
error,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchTokens,
|
request: fetchTokens,
|
||||||
result: { tokens, itemCount },
|
result: { tokens, itemCount, actions, relatedSearchFields },
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
const {
|
const [
|
||||||
data: { results, count },
|
{
|
||||||
} = await UsersAPI.readTokens(id, params);
|
data: { results, count },
|
||||||
|
},
|
||||||
|
actionsResponse,
|
||||||
|
] = await Promise.all([
|
||||||
|
UsersAPI.readTokens(id, params),
|
||||||
|
UsersAPI.readTokenOptions(id),
|
||||||
|
]);
|
||||||
const modifiedResults = results.map(result => {
|
const modifiedResults = results.map(result => {
|
||||||
result.summary_fields = {
|
result.summary_fields = {
|
||||||
user: result.summary_fields.user,
|
user: result.summary_fields.user,
|
||||||
@@ -44,9 +50,16 @@ function UserTokenList({ i18n }) {
|
|||||||
result.name = result.summary_fields.application?.name;
|
result.name = result.summary_fields.application?.name;
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
return { tokens: modifiedResults, itemCount: count };
|
return {
|
||||||
|
tokens: modifiedResults,
|
||||||
|
itemCount: count,
|
||||||
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchFields: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
|
};
|
||||||
}, [id, location.search]),
|
}, [id, location.search]),
|
||||||
{ tokens: [], itemCount: 0 }
|
{ tokens: [], itemCount: 0, actions: {}, relatedSearchFields: [] }
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -80,6 +93,10 @@ function UserTokenList({ i18n }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const canAdd = true;
|
const canAdd = true;
|
||||||
|
const relatedSearchableKeys = relatedSearchFields || [];
|
||||||
|
const searchableKeys = Object.keys(actions?.GET || {}).filter(
|
||||||
|
key => actions.GET[key].filterable
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
@@ -93,12 +110,12 @@ function UserTokenList({ i18n }) {
|
|||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
key: 'application__name',
|
key: 'application__name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Description`),
|
name: i18n._(t`Description`),
|
||||||
key: 'description',
|
key: 'description__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
@@ -123,6 +140,8 @@ function UserTokenList({ i18n }) {
|
|||||||
key: 'modified',
|
key: 'modified',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderToolbar={props => (
|
renderToolbar={props => (
|
||||||
<DataListToolbar
|
<DataListToolbar
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -123,8 +123,15 @@ const tokens = {
|
|||||||
|
|
||||||
describe('<UserTokenList />', () => {
|
describe('<UserTokenList />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
test('should mount properly, and fetch tokens', async () => {
|
|
||||||
|
beforeEach(() => {
|
||||||
UsersAPI.readTokens.mockResolvedValue(tokens);
|
UsersAPI.readTokens.mockResolvedValue(tokens);
|
||||||
|
UsersAPI.readTokenOptions.mockResolvedValue({
|
||||||
|
data: { related_search_fields: [] },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should mount properly, and fetch tokens', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<UserTokenList />);
|
wrapper = mountWithContexts(<UserTokenList />);
|
||||||
});
|
});
|
||||||
@@ -137,7 +144,6 @@ describe('<UserTokenList />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('edit button should be disabled', async () => {
|
test('edit button should be disabled', async () => {
|
||||||
UsersAPI.readTokens.mockResolvedValue(tokens);
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<UserTokenList />);
|
wrapper = mountWithContexts(<UserTokenList />);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user