mirror of
https://github.com/ansible/awx.git
synced 2026-05-09 02:17:37 -02:30
Merge pull request #8780 from mabashian/7879-notif-copy-search
Add support for notification template copy and advanced search Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -34,7 +34,7 @@ function CopyButton({
|
|||||||
<>
|
<>
|
||||||
<Tooltip content={helperText.tooltip} position="top">
|
<Tooltip content={helperText.tooltip} position="top">
|
||||||
<Button
|
<Button
|
||||||
isDisabled={isDisabled}
|
isDisabled={isLoading || isDisabled}
|
||||||
aria-label={i18n._(t`Copy`)}
|
aria-label={i18n._(t`Copy`)}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
onClick={copyItemToAPI}
|
onClick={copyItemToAPI}
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ function NotificationList({
|
|||||||
key: 'description__icontains',
|
key: 'description__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Type`),
|
name: i18n._(t`Notification type`),
|
||||||
key: 'or__notification_type',
|
key: 'or__notification_type',
|
||||||
options: [
|
options: [
|
||||||
['email', i18n._(t`Email`)],
|
['email', i18n._(t`Email`)],
|
||||||
|
|||||||
@@ -29,27 +29,41 @@ function NotificationTemplatesList({ i18n }) {
|
|||||||
const addUrl = `${match.url}/add`;
|
const addUrl = `${match.url}/add`;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { templates, count, actions },
|
result: {
|
||||||
|
templates,
|
||||||
|
count,
|
||||||
|
actions,
|
||||||
|
relatedSearchableKeys,
|
||||||
|
searchableKeys,
|
||||||
|
},
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading: isTemplatesLoading,
|
isLoading: isTemplatesLoading,
|
||||||
request: fetchTemplates,
|
request: fetchTemplates,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
const responses = await Promise.all([
|
const [response, actionsResponse] = await Promise.all([
|
||||||
NotificationTemplatesAPI.read(params),
|
NotificationTemplatesAPI.read(params),
|
||||||
NotificationTemplatesAPI.readOptions(),
|
NotificationTemplatesAPI.readOptions(),
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
templates: responses[0].data.results,
|
templates: response.data.results,
|
||||||
count: responses[0].data.count,
|
count: response.data.count,
|
||||||
actions: responses[1].data.actions,
|
actions: actionsResponse.data.actions,
|
||||||
|
relatedSearchableKeys: (
|
||||||
|
actionsResponse.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
|
searchableKeys: Object.keys(
|
||||||
|
actionsResponse.data.actions?.GET || {}
|
||||||
|
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
||||||
};
|
};
|
||||||
}, [location]),
|
}, [location]),
|
||||||
{
|
{
|
||||||
templates: [],
|
templates: [],
|
||||||
count: 0,
|
count: 0,
|
||||||
actions: {},
|
actions: {},
|
||||||
|
relatedSearchableKeys: [],
|
||||||
|
searchableKeys: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -109,7 +123,7 @@ function NotificationTemplatesList({ i18n }) {
|
|||||||
key: 'description__icontains',
|
key: 'description__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Type`),
|
name: i18n._(t`Notification type`),
|
||||||
key: 'or__notification_type',
|
key: 'or__notification_type',
|
||||||
options: [
|
options: [
|
||||||
['email', i18n._(t`Email`)],
|
['email', i18n._(t`Email`)],
|
||||||
@@ -125,14 +139,16 @@ function NotificationTemplatesList({ i18n }) {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Created By (Username)`),
|
name: i18n._(t`Created by (username)`),
|
||||||
key: 'created_by__username__icontains',
|
key: 'created_by__username__icontains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n._(t`Modified By (Username)`),
|
name: i18n._(t`Modified by (username)`),
|
||||||
key: 'modified_by__username__icontains',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
@@ -166,6 +182,7 @@ function NotificationTemplatesList({ i18n }) {
|
|||||||
renderItem={template => (
|
renderItem={template => (
|
||||||
<NotificationTemplateListItem
|
<NotificationTemplateListItem
|
||||||
key={template.id}
|
key={template.id}
|
||||||
|
fetchTemplates={fetchTemplates}
|
||||||
template={template}
|
template={template}
|
||||||
detailUrl={`${match.url}/${template.id}`}
|
detailUrl={`${match.url}/${template.id}`}
|
||||||
isSelected={selected.some(row => row.id === template.id)}
|
isSelected={selected.some(row => row.id === template.id)}
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { PencilAltIcon, BellIcon } from '@patternfly/react-icons';
|
import { PencilAltIcon, BellIcon } from '@patternfly/react-icons';
|
||||||
|
import { timeOfDay } from '../../../util/dates';
|
||||||
import { NotificationTemplatesAPI, NotificationsAPI } from '../../../api';
|
import { NotificationTemplatesAPI, NotificationsAPI } from '../../../api';
|
||||||
import DataListCell from '../../../components/DataListCell';
|
import DataListCell from '../../../components/DataListCell';
|
||||||
import StatusLabel from '../../../components/StatusLabel';
|
import StatusLabel from '../../../components/StatusLabel';
|
||||||
|
import CopyButton from '../../../components/CopyButton';
|
||||||
import useRequest from '../../../util/useRequest';
|
import useRequest from '../../../util/useRequest';
|
||||||
import { NOTIFICATION_TYPES } from '../constants';
|
import { NOTIFICATION_TYPES } from '../constants';
|
||||||
|
|
||||||
@@ -24,7 +26,7 @@ const DataListAction = styled(_DataListAction)`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 16px;
|
grid-gap: 16px;
|
||||||
grid-template-columns: 40px 40px;
|
grid-template-columns: repeat(3, 40px);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const NUM_RETRIES = 25;
|
const NUM_RETRIES = 25;
|
||||||
@@ -33,6 +35,7 @@ const RETRY_TIMEOUT = 5000;
|
|||||||
function NotificationTemplateListItem({
|
function NotificationTemplateListItem({
|
||||||
template,
|
template,
|
||||||
detailUrl,
|
detailUrl,
|
||||||
|
fetchTemplates,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
i18n,
|
i18n,
|
||||||
@@ -42,6 +45,22 @@ function NotificationTemplateListItem({
|
|||||||
? recentNotifications[0]?.status
|
? recentNotifications[0]?.status
|
||||||
: null;
|
: null;
|
||||||
const [status, setStatus] = useState(latestStatus);
|
const [status, setStatus] = useState(latestStatus);
|
||||||
|
const [isCopyDisabled, setIsCopyDisabled] = useState(false);
|
||||||
|
|
||||||
|
const copyTemplate = useCallback(async () => {
|
||||||
|
await NotificationTemplatesAPI.copy(template.id, {
|
||||||
|
name: `${template.name} @ ${timeOfDay()}`,
|
||||||
|
});
|
||||||
|
await fetchTemplates();
|
||||||
|
}, [template.id, template.name, fetchTemplates]);
|
||||||
|
|
||||||
|
const handleCopyStart = useCallback(() => {
|
||||||
|
setIsCopyDisabled(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCopyFinish = useCallback(() => {
|
||||||
|
setIsCopyDisabled(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setStatus(latestStatus);
|
setStatus(latestStatus);
|
||||||
@@ -114,7 +133,7 @@ function NotificationTemplateListItem({
|
|||||||
aria-label={i18n._(t`Test Notification`)}
|
aria-label={i18n._(t`Test Notification`)}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
onClick={sendTestNotification}
|
onClick={sendTestNotification}
|
||||||
disabled={isLoading}
|
isDisabled={isLoading || status === 'running'}
|
||||||
>
|
>
|
||||||
<BellIcon />
|
<BellIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -136,6 +155,18 @@ function NotificationTemplateListItem({
|
|||||||
) : (
|
) : (
|
||||||
<div />
|
<div />
|
||||||
)}
|
)}
|
||||||
|
{template.summary_fields.user_capabilities.copy && (
|
||||||
|
<CopyButton
|
||||||
|
copyItem={copyTemplate}
|
||||||
|
isCopyDisabled={isCopyDisabled}
|
||||||
|
onCopyStart={handleCopyStart}
|
||||||
|
onCopyFinish={handleCopyFinish}
|
||||||
|
helperText={{
|
||||||
|
tooltip: i18n._(t`Copy Notification Template`),
|
||||||
|
errorMessage: i18n._(t`Failed to copy template.`),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</DataListAction>
|
</DataListAction>
|
||||||
</DataListItemRow>
|
</DataListItemRow>
|
||||||
</DataListItem>
|
</DataListItem>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const template = {
|
|||||||
summary_fields: {
|
summary_fields: {
|
||||||
user_capabilities: {
|
user_capabilities: {
|
||||||
edit: true,
|
edit: true,
|
||||||
|
copy: true,
|
||||||
},
|
},
|
||||||
recent_notifications: [
|
recent_notifications: [
|
||||||
{
|
{
|
||||||
@@ -63,4 +64,56 @@ describe('<NotificationTemplateListItem />', () => {
|
|||||||
.text()
|
.text()
|
||||||
).toEqual('Running');
|
).toEqual('Running');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should call api to copy inventory', async () => {
|
||||||
|
NotificationTemplatesAPI.copy.mockResolvedValue();
|
||||||
|
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<NotificationTemplateListItem
|
||||||
|
template={template}
|
||||||
|
detailUrl="/notification_templates/3/detail"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[aria-label="Copy"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
expect(NotificationTemplatesAPI.copy).toHaveBeenCalled();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render proper alert modal on copy error', async () => {
|
||||||
|
NotificationTemplatesAPI.copy.mockRejectedValue(new Error());
|
||||||
|
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<NotificationTemplateListItem
|
||||||
|
template={template}
|
||||||
|
detailUrl="/notification_templates/3/detail"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[aria-label="Copy"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('Modal').prop('isOpen')).toBe(true);
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not render copy button', async () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<NotificationTemplateListItem
|
||||||
|
template={{
|
||||||
|
...template,
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
copy: false,
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
detailUrl="/notification_templates/3/detail"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('CopyButton').length).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user