mirror of
https://github.com/ansible/awx.git
synced 2024-10-27 00:55:06 +03:00
Adds inventory sources notifications list
This commit is contained in:
parent
fb8b90254c
commit
68a8dda869
@ -1,7 +1,8 @@
|
||||
import Base from '../Base';
|
||||
import NotificationsMixin from '../mixins/Notifications.mixin';
|
||||
import LaunchUpdateMixin from '../mixins/LaunchUpdate.mixin';
|
||||
|
||||
class InventorySources extends LaunchUpdateMixin(Base) {
|
||||
class InventorySources extends LaunchUpdateMixin(NotificationsMixin(Base)) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/inventory_sources/';
|
||||
|
@ -272,4 +272,39 @@ describe('<NotificationList />', () => {
|
||||
wrapper.find('NotificationList').state('startedTemplateIds')
|
||||
).toEqual([]);
|
||||
});
|
||||
test('should throw toggle error', async () => {
|
||||
MockModelAPI.associateNotificationTemplate.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'post',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
status: 403,
|
||||
},
|
||||
})
|
||||
);
|
||||
const wrapper = mountWithContexts(
|
||||
<NotificationList id={1} canToggleNotifications apiModel={MockModelAPI} />
|
||||
);
|
||||
await sleep(0);
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('NotificationList').state('startedTemplateIds')
|
||||
).toEqual([3]);
|
||||
const items = wrapper.find('NotificationListItem');
|
||||
items
|
||||
.at(0)
|
||||
.find('Switch[aria-label="Toggle notification start"]')
|
||||
.prop('onChange')();
|
||||
expect(MockModelAPI.associateNotificationTemplate).toHaveBeenCalledWith(
|
||||
1,
|
||||
1,
|
||||
'started'
|
||||
);
|
||||
await sleep(0);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ErrorDetail').length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
@ -19,6 +19,9 @@ const DataListAction = styled(_DataListAction)`
|
||||
grid-gap: 16px;
|
||||
grid-template-columns: repeat(3, max-content);
|
||||
`;
|
||||
const Label = styled.b`
|
||||
margin-right: 20px;
|
||||
`;
|
||||
|
||||
function NotificationListItem(props) {
|
||||
const {
|
||||
@ -54,6 +57,7 @@ function NotificationListItem(props) {
|
||||
</Link>
|
||||
</DataListCell>,
|
||||
<DataListCell key="type">
|
||||
<Label>{i18n._(t`Type `)}</Label>
|
||||
{typeLabels[notification.notification_type]}
|
||||
</DataListCell>,
|
||||
]}
|
||||
|
@ -55,7 +55,7 @@ describe('<NotificationListItem canToggleNotifications />', () => {
|
||||
.find('DataListCell')
|
||||
.at(1)
|
||||
.find('div');
|
||||
expect(typeCell.text()).toBe('Slack');
|
||||
expect(typeCell.text()).toContain('Slack');
|
||||
});
|
||||
|
||||
test('handles start click when toggle is on', () => {
|
||||
|
@ -58,6 +58,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
||||
</ForwardRef>
|
||||
</ForwardRef(Styled(PFDataListCell))>,
|
||||
<ForwardRef(Styled(PFDataListCell))>
|
||||
<ForwardRef(styled.b)>
|
||||
Type
|
||||
</ForwardRef(styled.b)>
|
||||
Slack
|
||||
</ForwardRef(Styled(PFDataListCell))>,
|
||||
]
|
||||
@ -167,6 +170,41 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
||||
<div
|
||||
className="pf-c-data-list__cell sc-bdVaJa kruorc"
|
||||
>
|
||||
<styled.b>
|
||||
<StyledComponent
|
||||
forwardedComponent={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"attrs": Array [],
|
||||
"componentStyle": ComponentStyle {
|
||||
"componentId": "sc-htpNat",
|
||||
"isStatic": false,
|
||||
"lastClassName": "jyYvCB",
|
||||
"rules": Array [
|
||||
"
|
||||
margin-right: 20px;
|
||||
",
|
||||
],
|
||||
},
|
||||
"displayName": "styled.b",
|
||||
"foldedComponentIds": Array [],
|
||||
"render": [Function],
|
||||
"styledComponentId": "sc-htpNat",
|
||||
"target": "b",
|
||||
"toString": [Function],
|
||||
"warnTooManyClasses": [Function],
|
||||
"withComponent": [Function],
|
||||
}
|
||||
}
|
||||
forwardedRef={null}
|
||||
>
|
||||
<b
|
||||
className="sc-htpNat jyYvCB"
|
||||
>
|
||||
Type
|
||||
</b>
|
||||
</StyledComponent>
|
||||
</styled.b>
|
||||
Slack
|
||||
</div>
|
||||
</PFDataListCell>
|
||||
|
@ -13,7 +13,11 @@ import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||
import { CardActions } from '@patternfly/react-core';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
|
||||
import { InventoriesAPI } from '../../../api';
|
||||
import {
|
||||
InventoriesAPI,
|
||||
InventorySourcesAPI,
|
||||
OrganizationsAPI,
|
||||
} from '../../../api';
|
||||
import { TabbedCardHeader } from '../../../components/Card';
|
||||
import CardCloseButton from '../../../components/CardCloseButton';
|
||||
import ContentError from '../../../components/ContentError';
|
||||
@ -21,20 +25,33 @@ import ContentLoading from '../../../components/ContentLoading';
|
||||
import RoutedTabs from '../../../components/RoutedTabs';
|
||||
import InventorySourceDetail from '../InventorySourceDetail';
|
||||
import InventorySourceEdit from '../InventorySourceEdit';
|
||||
import NotificationList from '../../../components/NotificationList/NotificationList';
|
||||
|
||||
function InventorySource({ i18n, inventory, setBreadcrumb }) {
|
||||
function InventorySource({ i18n, inventory, setBreadcrumb, me }) {
|
||||
const location = useLocation();
|
||||
const match = useRouteMatch('/inventories/inventory/:id/sources/:sourceId');
|
||||
const sourceListUrl = `/inventories/inventory/${inventory.id}/sources`;
|
||||
|
||||
const { result: source, error, isLoading, request: fetchSource } = useRequest(
|
||||
const {
|
||||
result: { source, isNotifAdmin },
|
||||
error,
|
||||
isLoading,
|
||||
request: fetchSource,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
return InventoriesAPI.readSourceDetail(
|
||||
inventory.id,
|
||||
match.params.sourceId
|
||||
);
|
||||
const [inventorySource, notifAdminRes] = await Promise.all([
|
||||
InventoriesAPI.readSourceDetail(inventory.id, match.params.sourceId),
|
||||
OrganizationsAPI.read({
|
||||
page_size: 1,
|
||||
role_level: 'notification_admin_role',
|
||||
}),
|
||||
]);
|
||||
return {
|
||||
source: inventorySource,
|
||||
isNotifAdmin: notifAdminRes.data.results.length > 0,
|
||||
};
|
||||
}, [inventory.id, match.params.sourceId]),
|
||||
null
|
||||
{ source: null, isNotifAdmin: false }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -63,18 +80,24 @@ function InventorySource({ i18n, inventory, setBreadcrumb }) {
|
||||
link: `${match.url}/details`,
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Notifications`),
|
||||
link: `${match.url}/notifications`,
|
||||
id: 2,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Schedules`),
|
||||
link: `${match.url}/schedules`,
|
||||
id: 3,
|
||||
id: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const canToggleNotifications = isNotifAdmin;
|
||||
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin;
|
||||
|
||||
if (canSeeNotificationsTab) {
|
||||
tabsArray.push({
|
||||
name: i18n._(t`Notifications`),
|
||||
link: `${match.url}/notifications`,
|
||||
id: 3,
|
||||
});
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ContentError error={error} />;
|
||||
}
|
||||
@ -111,6 +134,16 @@ function InventorySource({ i18n, inventory, setBreadcrumb }) {
|
||||
>
|
||||
<InventorySourceEdit source={source} inventory={inventory} />
|
||||
</Route>
|
||||
<Route
|
||||
key="notifications"
|
||||
path="/inventories/inventory/:id/sources/:sourceId/notifications"
|
||||
>
|
||||
<NotificationList
|
||||
id={Number(match.params.sourceId)}
|
||||
canToggleNotifications={canToggleNotifications}
|
||||
apiModel={InventorySourcesAPI}
|
||||
/>
|
||||
</Route>
|
||||
<Route key="not-found" path="*">
|
||||
<ContentError isNotFound>
|
||||
<Link to={`${match.url}/details`}>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { InventoriesAPI } from '../../../api';
|
||||
import { InventoriesAPI, OrganizationsAPI } from '../../../api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
@ -10,6 +10,9 @@ import mockInventorySource from '../shared/data.inventory_source.json';
|
||||
import InventorySource from './InventorySource';
|
||||
|
||||
jest.mock('../../../api/models/Inventories');
|
||||
jest.mock('../../../api/models/Organizations');
|
||||
jest.mock('../../../api/models/InventorySources');
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useRouteMatch: () => ({
|
||||
@ -18,10 +21,6 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
InventoriesAPI.readSourceDetail.mockResolvedValue({
|
||||
data: { ...mockInventorySource },
|
||||
});
|
||||
|
||||
const mockInventory = {
|
||||
id: 2,
|
||||
name: 'Mock Inventory',
|
||||
@ -34,22 +33,31 @@ describe('<InventorySource />', () => {
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventorySource inventory={mockInventory} setBreadcrumb={() => {}} />
|
||||
<InventorySource
|
||||
inventory={mockInventory}
|
||||
me={{ is_system_auditor: false }}
|
||||
setBreadcrumb={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('should render expected tabs', () => {
|
||||
InventoriesAPI.readSourceDetail.mockResolvedValue({
|
||||
data: { ...mockInventorySource },
|
||||
});
|
||||
OrganizationsAPI.read.mockResolvedValue({
|
||||
data: { results: [{ id: 1, name: 'isNotifAdmin' }] },
|
||||
});
|
||||
const expectedTabs = [
|
||||
'Back to Sources',
|
||||
'Details',
|
||||
'Notifications',
|
||||
'Schedules',
|
||||
'Notifications',
|
||||
];
|
||||
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||
@ -57,10 +65,20 @@ describe('<InventorySource />', () => {
|
||||
});
|
||||
|
||||
test('should show content error when api throws error on initial render', async () => {
|
||||
InventoriesAPI.readSourceDetail.mockResolvedValue({
|
||||
data: { ...mockInventorySource },
|
||||
});
|
||||
OrganizationsAPI.read.mockResolvedValue({
|
||||
data: { results: [{ id: 1, name: 'isNotifAdmin' }] },
|
||||
});
|
||||
InventoriesAPI.readSourceDetail.mockRejectedValueOnce(new Error());
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventorySource inventory={mockInventory} setBreadcrumb={() => {}} />
|
||||
<InventorySource
|
||||
inventory={mockInventory}
|
||||
me={{ is_system_auditor: false }}
|
||||
setBreadcrumb={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
@ -71,16 +89,47 @@ describe('<InventorySource />', () => {
|
||||
});
|
||||
|
||||
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
||||
InventoriesAPI.readSourceDetail.mockResolvedValue({
|
||||
data: { ...mockInventorySource },
|
||||
});
|
||||
OrganizationsAPI.read.mockResolvedValue({
|
||||
data: { results: [{ id: 1, name: 'isNotifAdmin' }] },
|
||||
});
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/inventory/2/sources/1/foobar'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventorySource inventory={mockInventory} setBreadcrumb={() => {}} />,
|
||||
<InventorySource
|
||||
inventory={mockInventory}
|
||||
setBreadcrumb={() => {}}
|
||||
me={{ is_system_auditor: false }}
|
||||
/>,
|
||||
{ context: { router: { history } } }
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
expect(wrapper.find('ContentError Title').text()).toEqual('Not Found');
|
||||
});
|
||||
|
||||
test('should call api', () => {
|
||||
InventoriesAPI.readSourceDetail.mockResolvedValue({
|
||||
data: { ...mockInventorySource },
|
||||
});
|
||||
OrganizationsAPI.read.mockResolvedValue({
|
||||
data: { results: [{ id: 1, name: 'isNotifAdmin' }] },
|
||||
});
|
||||
expect(InventoriesAPI.readSourceDetail).toBeCalledWith(2, 123);
|
||||
expect(OrganizationsAPI.read).toBeCalled();
|
||||
});
|
||||
|
||||
test('should not render notifications tab', () => {
|
||||
InventoriesAPI.readSourceDetail.mockResolvedValue({
|
||||
data: { ...mockInventorySource },
|
||||
});
|
||||
OrganizationsAPI.read.mockResolvedValue({
|
||||
data: { results: [] },
|
||||
});
|
||||
expect(wrapper.find('button[aria-label="Notifications"]').length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import InventorySource from '../InventorySource';
|
||||
import { Config } from '../../../contexts/Config';
|
||||
import InventorySourceAdd from '../InventorySourceAdd';
|
||||
import InventorySourceList from './InventorySourceList';
|
||||
|
||||
@ -11,7 +12,15 @@ function InventorySources({ inventory, setBreadcrumb }) {
|
||||
<InventorySourceAdd />
|
||||
</Route>
|
||||
<Route path="/inventories/inventory/:id/sources/:sourceId">
|
||||
<InventorySource inventory={inventory} setBreadcrumb={setBreadcrumb} />
|
||||
<Config>
|
||||
{({ me }) => (
|
||||
<InventorySource
|
||||
inventory={inventory}
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
me={me || {}}
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
</Route>
|
||||
<Route path="/inventories/:inventoryType/:id/sources">
|
||||
<InventorySourceList />
|
||||
|
Loading…
Reference in New Issue
Block a user