Adds smart inventory button on host list

This commit is contained in:
mabashian 2020-12-14 12:01:58 -05:00
parent 684998cd51
commit 87604749b7
4 changed files with 137 additions and 7 deletions

View File

@ -1,8 +1,8 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useLocation, useRouteMatch } from 'react-router-dom';
import { useHistory, useLocation, useRouteMatch } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Card, PageSection } from '@patternfly/react-core';
import { Button, Card, PageSection, Tooltip } from '@patternfly/react-core';
import { HostsAPI } from '../../../api';
import AlertModal from '../../../components/AlertModal';
@ -13,7 +13,11 @@ import PaginatedDataList, {
ToolbarDeleteButton,
} from '../../../components/PaginatedDataList';
import useRequest, { useDeleteItems } from '../../../util/useRequest';
import { getQSConfig, parseQueryString } from '../../../util/qs';
import {
encodeQueryString,
getQSConfig,
parseQueryString,
} from '../../../util/qs';
import HostListItem from './HostListItem';
@ -24,9 +28,21 @@ const QS_CONFIG = getQSConfig('host', {
});
function HostList({ i18n }) {
const history = useHistory();
const location = useLocation();
const match = useRouteMatch();
const [selected, setSelected] = useState([]);
const parsedQueryStrings = parseQueryString(QS_CONFIG, location.search);
const nonDefaultSearchParams = {};
Object.keys(parsedQueryStrings).forEach(key => {
if (!QS_CONFIG.defaultParams[key]) {
nonDefaultSearchParams[key] = parsedQueryStrings[key];
}
});
const hasNonDefaultSearchParams =
Object.keys(nonDefaultSearchParams).length > 0;
const {
result: { hosts, count, actions, relatedSearchableKeys, searchableKeys },
@ -99,6 +115,14 @@ function HostList({ i18n }) {
}
};
const handleSmartInventoryClick = () => {
history.push(
`/inventories/smart_inventory/add?host_filter=${encodeURIComponent(
encodeQueryString(nonDefaultSearchParams)
)}`
);
};
const canAdd =
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
@ -157,6 +181,34 @@ function HostList({ i18n }) {
itemsToDelete={selected}
pluralizedItemName={i18n._(t`Hosts`)}
/>,
...(canAdd
? [
<Tooltip
key="smartInventory"
content={
hasNonDefaultSearchParams
? i18n._(
t`Create a new Smart Inventory with the applied filter`
)
: i18n._(
t`Enter at least one search filter to create a new Smart Inventory`
)
}
position="top"
>
<div>
<Button
onClick={() => handleSmartInventoryClick()}
aria-label={i18n._(t`Smart Inventory`)}
variant="secondary"
isDisabled={!hasNonDefaultSearchParams}
>
{i18n._(t`Smart Inventory`)}
</Button>
</div>
</Tooltip>,
]
: []),
]}
/>
)}

View File

@ -1,5 +1,6 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { HostsAPI } from '../../../api';
import {
mountWithContexts,
@ -257,7 +258,7 @@ describe('<HostList />', () => {
expect(modal.prop('title')).toEqual('Error!');
});
test('should show Add button according to permissions', async () => {
test('should show Add and Smart Inventory buttons according to permissions', async () => {
let wrapper;
await act(async () => {
wrapper = mountWithContexts(<HostList />);
@ -265,9 +266,10 @@ describe('<HostList />', () => {
await waitForLoaded(wrapper);
expect(wrapper.find('ToolbarAddButton').length).toBe(1);
expect(wrapper.find('Button[aria-label="Smart Inventory"]').length).toBe(1);
});
test('should hide Add button according to permissions', async () => {
test('should hide Add and Smart Inventory buttons according to permissions', async () => {
HostsAPI.readOptions.mockResolvedValue({
data: {
actions: {
@ -282,5 +284,44 @@ describe('<HostList />', () => {
await waitForLoaded(wrapper);
expect(wrapper.find('ToolbarAddButton').length).toBe(0);
expect(wrapper.find('Button[aria-label="Smart Inventory"]').length).toBe(0);
});
test('Smart Inventory button should be disabled when no search params are present', async () => {
let wrapper;
await act(async () => {
wrapper = mountWithContexts(<HostList />);
});
await waitForLoaded(wrapper);
expect(
wrapper.find('Button[aria-label="Smart Inventory"]').props().isDisabled
).toBe(true);
});
test('Clicking Smart Inventory button should navigate to smart inventory form with correct query param', async () => {
let wrapper;
const history = createMemoryHistory({
initialEntries: ['/hosts?host.name__icontains=foo'],
});
await act(async () => {
wrapper = mountWithContexts(<HostList />, {
context: { router: { history } },
});
});
await waitForLoaded(wrapper);
expect(
wrapper.find('Button[aria-label="Smart Inventory"]').props().isDisabled
).toBe(false);
await act(async () => {
wrapper.find('Button[aria-label="Smart Inventory"]').simulate('click');
});
wrapper.update();
expect(history.location.pathname).toEqual(
'/inventories/smart_inventory/add'
);
expect(history.location.search).toEqual(
'?host_filter=name__icontains%3Dfoo'
);
});
});

View File

@ -2,7 +2,8 @@ import React, { useEffect, useCallback } from 'react';
import { Formik, useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { func, shape, arrayOf } from 'prop-types';
import { useLocation } from 'react-router-dom';
import { func, shape, object, arrayOf } from 'prop-types';
import { Form } from '@patternfly/react-core';
import { InstanceGroup } from '../../../types';
import { VariablesField } from '../../../components/CodeMirrorInput';
@ -14,6 +15,10 @@ import {
FormColumnLayout,
FormFullWidthLayout,
} from '../../../components/FormLayout';
import {
toHostFilter,
toSearchParams,
} from '../../../components/Lookup/shared/HostFilterUtils';
import HostFilterLookup from '../../../components/Lookup/HostFilterLookup';
import InstanceGroupsLookup from '../../../components/Lookup/InstanceGroupsLookup';
import OrganizationLookup from '../../../components/Lookup/OrganizationLookup';
@ -109,9 +114,17 @@ function SmartInventoryForm({
onCancel,
submitError,
}) {
const { search } = useLocation();
const queryParams = new URLSearchParams(search);
const hostFilterFromParams = queryParams.get('host_filter');
const initialValues = {
description: inventory.description || '',
host_filter: inventory.host_filter || '',
host_filter:
inventory.host_filter ||
(hostFilterFromParams
? toHostFilter(toSearchParams(hostFilterFromParams))
: ''),
instance_groups: instanceGroups || [],
kind: 'smart',
name: inventory.name || '',

View File

@ -1,5 +1,6 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import {
mountWithContexts,
waitForElement,
@ -135,6 +136,29 @@ describe('<SmartInventoryForm />', () => {
});
});
test('should pre-fill the host filter when query param present and not editing', async () => {
let wrapper;
const history = createMemoryHistory({
initialEntries: [
'/inventories/smart_inventory/add?host_filter=name__icontains%3Dfoo',
],
});
await act(async () => {
wrapper = mountWithContexts(
<SmartInventoryForm onCancel={() => {}} onSubmit={() => {}} />,
{
context: { router: { history } },
}
);
});
wrapper.update();
const nameChipGroup = wrapper.find(
'HostFilterLookup ChipGroup[categoryName="Name"]'
);
expect(nameChipGroup.find('Chip').length).toBe(1);
wrapper.unmount();
});
test('should throw content error when option request fails', async () => {
let wrapper;
InventoriesAPI.readOptions.mockImplementationOnce(() =>