diff --git a/awx/ui/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.js b/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.js similarity index 66% rename from awx/ui/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.js rename to awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.js index eb8d690d96..98f890ed12 100644 --- a/awx/ui/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.js +++ b/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.js @@ -1,7 +1,7 @@ import React, { useCallback, useEffect } from 'react'; -import { useLocation, useParams } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; -import { t } from '@lingui/macro'; +import { t, Plural } from '@lingui/macro'; import { Card } from '@patternfly/react-core'; import { JobTemplatesAPI } from 'api'; import AlertModal from 'components/AlertModal'; @@ -14,10 +14,14 @@ import PaginatedTable, { ToolbarDeleteButton, getSearchableKeys, } from 'components/PaginatedTable'; -import { getQSConfig, parseQueryString } from 'util/qs'; +import { getQSConfig, parseQueryString, mergeParams } from 'util/qs'; +import useWsTemplates from 'hooks/useWsTemplates'; import useSelected from 'hooks/useSelected'; +import useExpanded from 'hooks/useExpanded'; import useRequest, { useDeleteItems } from 'hooks/useRequest'; -import ProjectTemplatesListItem from './ProjectJobTemplatesListItem'; +import { TemplateListItem } from 'components/TemplateList'; +import useToast, { AlertVariant } from 'hooks/useToast'; +import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails'; const QS_CONFIG = getQSConfig('template', { page: 1, @@ -25,13 +29,13 @@ const QS_CONFIG = getQSConfig('template', { order_by: 'name', }); -function ProjectJobTemplatesList() { - const { id: projectId } = useParams(); +function RelatedTemplateList({ searchParams }) { const location = useLocation(); + const { addToast, Toast, toastProps } = useToast(); const { result: { - jobTemplates, + results, itemCount, actions, relatedSearchableKeys, @@ -43,13 +47,12 @@ function ProjectJobTemplatesList() { } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - params.project = projectId; const [response, actionsResponse] = await Promise.all([ - JobTemplatesAPI.read(params), + JobTemplatesAPI.read(mergeParams(params, searchParams)), JobTemplatesAPI.readOptions(), ]); return { - jobTemplates: response.data.results, + results: response.data.results, itemCount: response.data.count, actions: actionsResponse.data.actions, relatedSearchableKeys: ( @@ -57,9 +60,9 @@ function ProjectJobTemplatesList() { ).map((val) => val.slice(0, -8)), searchableKeys: getSearchableKeys(actionsResponse.data.actions?.GET), }; - }, [location, projectId]), + }, [location]), // eslint-disable-line react-hooks/exhaustive-deps { - jobTemplates: [], + results: [], itemCount: 0, actions: {}, relatedSearchableKeys: [], @@ -71,9 +74,14 @@ function ProjectJobTemplatesList() { fetchTemplates(); }, [fetchTemplates]); + const jobTemplates = useWsTemplates(results); + const { selected, isAllSelected, handleSelect, clearSelected, selectAll } = useSelected(jobTemplates); + const { expanded, isAllExpanded, handleExpand, expandAll } = + useExpanded(jobTemplates); + const { isLoading: isDeleteLoading, deleteItems: deleteTemplates, @@ -94,6 +102,18 @@ function ProjectJobTemplatesList() { } ); + const handleCopy = useCallback( + (newTemplateId) => { + addToast({ + id: newTemplateId, + title: t`Template copied successfully`, + variant: AlertVariant.success, + hasTimeout: true, + }); + }, + [addToast] + ); + const handleTemplateDelete = async () => { await deleteTemplates(); clearSelected(); @@ -106,6 +126,10 @@ function ProjectJobTemplatesList() { ); + const deleteDetailsRequests = relatedResourceDeleteRequests.template( + selected[0] + ); + return ( <> @@ -131,14 +155,32 @@ function ProjectJobTemplatesList() { name: t`Modified By (Username)`, key: 'modified_by__username__icontains', }, + { + name: t`Playbook name`, + key: 'job_template__playbook__icontains', + }, + { + name: t`Label`, + key: 'labels__name__icontains', + }, ]} toolbarSearchableKeys={searchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys} + headerRow={ + + {t`Name`} + {t`Type`} + {t`Recent jobs`} + {t`Actions`} + + } renderToolbar={(props) => ( + } />, ]} /> )} - headerRow={ - - {t`Name`} - {t`Type`} - {t`Recent jobs`} - {t`Actions`} - - } renderRow={(template, index) => ( - handleSelect(template)} + isExpanded={expanded.some((row) => row.id === template.id)} + onExpand={() => handleExpand(template)} + onCopy={handleCopy} isSelected={selected.some((row) => row.id === template.id)} + fetchTemplates={fetchTemplates} rowIndex={index} /> )} emptyStateControls={canAddJT && addButton} /> + ', () => { + let debug; + beforeEach(() => { + JobTemplatesAPI.read.mockResolvedValue({ + data: { + count: mockTemplates.length, + results: mockTemplates, + }, + }); + + JobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: [], + }, + }); + debug = global.console.debug; // eslint-disable-line prefer-destructuring + global.console.debug = () => {}; + }); + + afterEach(() => { + jest.clearAllMocks(); + global.console.debug = debug; + }); + + test('Templates are retrieved from the api and the components finishes loading', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(JobTemplatesAPI.read).toBeCalledWith({ + credentials__id: 1, + order_by: 'name', + page: 1, + page_size: 20, + }); + await act(async () => { + await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); + }); + expect(wrapper.find('TemplateListItem').length).toEqual( + mockTemplates.length + ); + }); + + test('handleSelect is called when a template list item is selected', async () => { + const wrapper = mountWithContexts( + + ); + await act(async () => { + await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); + }); + const checkBox = wrapper.find('TemplateListItem').at(1).find('input'); + + checkBox.simulate('change', { + target: { + id: 2, + name: 'Job Template 2', + url: '/templates/job_template/2', + type: 'job_template', + summary_fields: { user_capabilities: { delete: true } }, + }, + }); + + expect(wrapper.find('TemplateListItem').at(1).prop('isSelected')).toBe( + true + ); + }); + + test('handleSelectAll is called when a template list item is selected', async () => { + const wrapper = mountWithContexts( + + ); + await act(async () => { + await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); + }); + expect(wrapper.find('Checkbox#select-all').prop('isChecked')).toBe(false); + + const toolBarCheckBox = wrapper.find('Checkbox#select-all'); + act(() => { + toolBarCheckBox.prop('onChange')(true); + }); + wrapper.update(); + expect(wrapper.find('Checkbox#select-all').prop('isChecked')).toBe(true); + }); + + test('delete button is disabled if user does not have delete capabilities on a selected template', async () => { + const wrapper = mountWithContexts( + + ); + await act(async () => { + await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); + }); + const deleteAbleItem = wrapper.find('TemplateListItem').at(0).find('input'); + const nonDeleteAbleItem = wrapper + .find('TemplateListItem') + .at(2) + .find('input'); + + deleteAbleItem.simulate('change', { + id: 1, + name: 'Job Template 1', + url: '/templates/job_template/1', + type: 'job_template', + summary_fields: { + user_capabilities: { + delete: true, + }, + }, + }); + + expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe( + false + ); + deleteAbleItem.simulate('change', { + id: 1, + name: 'Job Template 1', + url: '/templates/job_template/1', + type: 'job_template', + summary_fields: { + user_capabilities: { + delete: true, + }, + }, + }); + expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe( + true + ); + nonDeleteAbleItem.simulate('change', { + id: 5, + name: 'Workflow Job Template 2', + url: '/templates/workflow_job_template/5', + type: 'workflow_job_template', + summary_fields: { + user_capabilities: { + delete: false, + }, + }, + }); + expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe( + true + ); + }); + + test('api is called to delete templates for each selected template.', async () => { + const wrapper = mountWithContexts( + + ); + await act(async () => { + await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); + }); + const jobTemplate = wrapper.find('TemplateListItem').at(1).find('input'); + + jobTemplate.simulate('change', { + target: { + id: 2, + name: 'Job Template 2', + url: '/templates/job_template/2', + type: 'job_template', + summary_fields: { user_capabilities: { delete: true } }, + }, + }); + + await act(async () => { + wrapper.find('button[aria-label="Delete"]').prop('onClick')(); + }); + wrapper.update(); + await act(async () => { + await wrapper + .find('button[aria-label="confirm delete"]') + .prop('onClick')(); + }); + expect(JobTemplatesAPI.destroy).toBeCalledWith(2); + }); + + test('error is shown when template not successfully deleted from api', async () => { + JobTemplatesAPI.destroy.mockRejectedValue( + new Error({ + response: { + config: { + method: 'delete', + url: '/api/v2/job_templates/1', + }, + data: 'An error occurred', + }, + }) + ); + let wrapper; + + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + wrapper.update(); + expect(JobTemplatesAPI.read).toHaveBeenCalledTimes(1); + + await act(async () => { + wrapper.find('TemplateListItem').at(0).invoke('onSelect')(); + }); + wrapper.update(); + + await act(async () => { + wrapper.find('ToolbarDeleteButton').invoke('onDelete')(); + }); + wrapper.update(); + + const modal = wrapper.find('Modal'); + expect(modal).toHaveLength(1); + expect(modal.prop('title')).toEqual('Error!'); + }); + + test('should properly copy template', async () => { + JobTemplatesAPI.copy.mockResolvedValue({}); + const wrapper = mountWithContexts( + + ); + await act(async () => { + await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); + }); + await act(async () => + wrapper.find('Button[aria-label="Copy"]').prop('onClick')() + ); + expect(JobTemplatesAPI.copy).toHaveBeenCalled(); + }); +}); diff --git a/awx/ui/src/components/RelatedTemplateList/index.js b/awx/ui/src/components/RelatedTemplateList/index.js new file mode 100644 index 0000000000..af65c11b6f --- /dev/null +++ b/awx/ui/src/components/RelatedTemplateList/index.js @@ -0,0 +1 @@ +export { default } from './RelatedTemplateList'; diff --git a/awx/ui/src/screens/Credential/Credential.js b/awx/ui/src/screens/Credential/Credential.js index 2304737dca..ba580fd76e 100644 --- a/awx/ui/src/screens/Credential/Credential.js +++ b/awx/ui/src/screens/Credential/Credential.js @@ -17,6 +17,7 @@ import { ResourceAccessList } from 'components/ResourceAccessList'; import ContentError from 'components/ContentError'; import ContentLoading from 'components/ContentLoading'; import RoutedTabs from 'components/RoutedTabs'; +import RelatedTemplateList from 'components/RelatedTemplateList'; import { CredentialsAPI } from 'api'; import CredentialDetail from './CredentialDetail'; import CredentialEdit from './CredentialEdit'; @@ -73,6 +74,11 @@ function Credential({ setBreadcrumb }) { link: `/credentials/${id}/access`, id: 1, }, + { + name: t`Job Templates`, + link: `/credentials/${id}/job_templates`, + id: 2, + }, ]; let showCardHeader = true; @@ -123,6 +129,11 @@ function Credential({ setBreadcrumb }) { apiModel={CredentialsAPI} /> , + + + , {!hasContentLoading && ( diff --git a/awx/ui/src/screens/Credential/Credential.test.js b/awx/ui/src/screens/Credential/Credential.test.js index a4e9cf6b68..b66619c877 100644 --- a/awx/ui/src/screens/Credential/Credential.test.js +++ b/awx/ui/src/screens/Credential/Credential.test.js @@ -7,7 +7,6 @@ import { waitForElement, } from '../../../testUtils/enzymeHelpers'; import mockCredential from './shared/data.scmCredential.json'; -import mockOrgCredential from './shared/data.orgCredential.json'; import Credential from './Credential'; jest.mock('../../api'); @@ -32,21 +31,24 @@ describe('', () => { await act(async () => { wrapper = mountWithContexts( {}} />); }); - await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); - await waitForElement(wrapper, '.pf-c-tabs__item', (el) => el.length === 3); + wrapper.update(); + expect(wrapper.find('Credential').length).toBe(1); + expect(wrapper.find('RoutedTabs li').length).toBe(4); }); - test('initially renders org-based credential successfully', async () => { - CredentialsAPI.readDetail.mockResolvedValueOnce({ - data: mockOrgCredential, - }); - + test('should render expected tabs', async () => { + const expectedTabs = [ + 'Back to Credentials', + 'Details', + 'Access', + 'Job Templates', + ]; await act(async () => { wrapper = mountWithContexts( {}} />); }); - await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); - // org-based credential detail needs access tab - await waitForElement(wrapper, '.pf-c-tabs__item', (el) => el.length === 3); + wrapper.find('RoutedTabs li').forEach((tab, index) => { + expect(tab.text()).toEqual(expectedTabs[index]); + }); }); test('should show content error when user attempts to navigate to erroneous route', async () => { diff --git a/awx/ui/src/screens/Credential/Credentials.js b/awx/ui/src/screens/Credential/Credentials.js index e95ce0c853..cb213b5534 100644 --- a/awx/ui/src/screens/Credential/Credentials.js +++ b/awx/ui/src/screens/Credential/Credentials.js @@ -26,6 +26,7 @@ function Credentials() { [`/credentials/${credential.id}/edit`]: t`Edit Details`, [`/credentials/${credential.id}/details`]: t`Details`, [`/credentials/${credential.id}/access`]: t`Access`, + [`/credentials/${credential.id}/job_templates`]: t`Job Templates`, }); }, []); diff --git a/awx/ui/src/screens/Inventory/Inventories.js b/awx/ui/src/screens/Inventory/Inventories.js index 815f51cd34..cb4e51b712 100644 --- a/awx/ui/src/screens/Inventory/Inventories.js +++ b/awx/ui/src/screens/Inventory/Inventories.js @@ -58,6 +58,7 @@ function Inventories() { [`${inventoryPath}/access`]: t`Access`, [`${inventoryPath}/jobs`]: t`Jobs`, [`${inventoryPath}/details`]: t`Details`, + [`${inventoryPath}/job_templates`]: t`Job Templates`, [`${inventoryPath}/edit`]: t`Edit details`, [inventoryHostsPath]: t`Hosts`, diff --git a/awx/ui/src/screens/Inventory/Inventory.js b/awx/ui/src/screens/Inventory/Inventory.js index 2e18bf1811..c0bf58c39b 100644 --- a/awx/ui/src/screens/Inventory/Inventory.js +++ b/awx/ui/src/screens/Inventory/Inventory.js @@ -16,6 +16,7 @@ import ContentLoading from 'components/ContentLoading'; import JobList from 'components/JobList'; import RoutedTabs from 'components/RoutedTabs'; import { ResourceAccessList } from 'components/ResourceAccessList'; +import RelatedTemplateList from 'components/RelatedTemplateList'; import { InventoriesAPI } from 'api'; import InventoryDetail from './InventoryDetail'; import InventoryEdit from './InventoryEdit'; @@ -69,6 +70,7 @@ function Inventory({ setBreadcrumb }) { link: `${match.url}/jobs`, id: 5, }, + { name: t`Job Templates`, link: `${match.url}/job_templates`, id: 6 }, ]; if (hasContentLoading) { @@ -172,6 +174,14 @@ function Inventory({ setBreadcrumb }) { ]} /> , + + + , {match.params.id && ( diff --git a/awx/ui/src/screens/Inventory/Inventory.test.js b/awx/ui/src/screens/Inventory/Inventory.test.js index ee35ad6139..55a37b6c39 100644 --- a/awx/ui/src/screens/Inventory/Inventory.test.js +++ b/awx/ui/src/screens/Inventory/Inventory.test.js @@ -31,8 +31,27 @@ describe('', () => { await act(async () => { wrapper = mountWithContexts( {}} />); }); - await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); - await waitForElement(wrapper, '.pf-c-tabs__item', (el) => el.length === 7); + wrapper.update(); + expect(wrapper.find('Inventory').length).toBe(1); + expect(wrapper.find('RoutedTabs li').length).toBe(8); + }); + + test('should render expected tabs', async () => { + const expectedTabs = [ + 'Back to Inventories', + 'Details', + 'Access', + 'Groups', + 'Hosts', + 'Jobs', + 'Job Templates', + ]; + await act(async () => { + wrapper = mountWithContexts( {}} />); + }); + wrapper.find('RoutedTabs li').forEach((tab, index) => { + expect(tab.text()).toEqual(expectedTabs[index]); + }); }); test('should show content error when user attempts to navigate to erroneous route', async () => { diff --git a/awx/ui/src/screens/Inventory/SmartInventory.js b/awx/ui/src/screens/Inventory/SmartInventory.js index 146c0e3868..952cf5dc31 100644 --- a/awx/ui/src/screens/Inventory/SmartInventory.js +++ b/awx/ui/src/screens/Inventory/SmartInventory.js @@ -19,6 +19,7 @@ import ContentLoading from 'components/ContentLoading'; import JobList from 'components/JobList'; import { ResourceAccessList } from 'components/ResourceAccessList'; import RoutedTabs from 'components/RoutedTabs'; +import RelatedTemplateList from 'components/RelatedTemplateList'; import SmartInventoryDetail from './SmartInventoryDetail'; import SmartInventoryEdit from './SmartInventoryEdit'; import SmartInventoryHosts from './SmartInventoryHosts'; @@ -70,6 +71,7 @@ function SmartInventory({ setBreadcrumb }) { link: `${match.url}/jobs`, id: 3, }, + { name: t`Job Templates`, link: `${match.url}/job_templates`, id: 4 }, ]; if (hasContentLoading) { @@ -155,6 +157,14 @@ function SmartInventory({ setBreadcrumb }) { }} /> , + + + , {!hasContentLoading && ( diff --git a/awx/ui/src/screens/Inventory/SmartInventory.test.js b/awx/ui/src/screens/Inventory/SmartInventory.test.js index 13bb6426c7..9d5ae971c4 100644 --- a/awx/ui/src/screens/Inventory/SmartInventory.test.js +++ b/awx/ui/src/screens/Inventory/SmartInventory.test.js @@ -32,8 +32,26 @@ describe('', () => { await act(async () => { wrapper = mountWithContexts( {}} />); }); - await waitForElement(wrapper, 'SmartInventory'); - await waitForElement(wrapper, '.pf-c-tabs__item', (el) => el.length === 5); + wrapper.update(); + expect(wrapper.find('SmartInventory').length).toBe(1); + expect(wrapper.find('RoutedTabs li').length).toBe(6); + }); + + test('should render expected tabs', async () => { + const expectedTabs = [ + 'Back to Inventories', + 'Details', + 'Access', + 'Hosts', + 'Jobs', + 'Job Templates', + ]; + await act(async () => { + wrapper = mountWithContexts( {}} />); + }); + wrapper.find('RoutedTabs li').forEach((tab, index) => { + expect(tab.text()).toEqual(expectedTabs[index]); + }); }); test('should show content error when api throws an error', async () => { diff --git a/awx/ui/src/screens/Project/Project.js b/awx/ui/src/screens/Project/Project.js index 8426ef59df..a3156ca151 100644 --- a/awx/ui/src/screens/Project/Project.js +++ b/awx/ui/src/screens/Project/Project.js @@ -19,10 +19,10 @@ import ContentLoading from 'components/ContentLoading'; import NotificationList from 'components/NotificationList'; import { ResourceAccessList } from 'components/ResourceAccessList'; import { Schedules } from 'components/Schedule'; +import RelatedTemplateList from 'components/RelatedTemplateList'; import { OrganizationsAPI, ProjectsAPI } from 'api'; import ProjectDetail from './ProjectDetail'; import ProjectEdit from './ProjectEdit'; -import ProjectJobTemplatesList from './ProjectJobTemplatesList'; function Project({ setBreadcrumb }) { const { me = {} } = useConfig(); @@ -102,6 +102,10 @@ function Project({ setBreadcrumb }) { }, { name: t`Details`, link: `/projects/${id}/details` }, { name: t`Access`, link: `/projects/${id}/access` }, + { + name: t`Job Templates`, + link: `/projects/${id}/job_templates`, + }, ]; if (canSeeNotificationsTab) { @@ -110,12 +114,6 @@ function Project({ setBreadcrumb }) { link: `/projects/${id}/notifications`, }); } - - tabsArray.push({ - name: t`Job Templates`, - link: `/projects/${id}/job_templates`, - }); - if (project?.scm_type) { tabsArray.push({ name: t`Schedules`, @@ -176,7 +174,7 @@ function Project({ setBreadcrumb }) { )} - + {project?.scm_type && project.scm_type !== '' && ( diff --git a/awx/ui/src/screens/Project/Project.test.js b/awx/ui/src/screens/Project/Project.test.js index 4bd7689eba..48621a31ac 100644 --- a/awx/ui/src/screens/Project/Project.test.js +++ b/awx/ui/src/screens/Project/Project.test.js @@ -63,7 +63,7 @@ describe('', () => { '.pf-c-tabs__item-text', (el) => el.length === 6 ); - expect(tabs.at(3).text()).toEqual('Notifications'); + expect(tabs.at(4).text()).toEqual('Notifications'); }); test('notifications tab hidden with reduced permissions', async () => { diff --git a/awx/ui/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.js b/awx/ui/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.js deleted file mode 100644 index 199a629c77..0000000000 --- a/awx/ui/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.js +++ /dev/null @@ -1,121 +0,0 @@ -import 'styled-components/macro'; -import React from 'react'; -import { Link } from 'react-router-dom'; -import { Button, Tooltip } from '@patternfly/react-core'; -import { Tr, Td } from '@patternfly/react-table'; -import { - ExclamationTriangleIcon, - PencilAltIcon, - RocketIcon, -} from '@patternfly/react-icons'; -import { t } from '@lingui/macro'; -import styled from 'styled-components'; - -import { ActionsTd, ActionItem } from 'components/PaginatedTable'; -import { LaunchButton } from 'components/LaunchButton'; -import Sparkline from 'components/Sparkline'; -import { toTitleCase } from 'util/strings'; - -const ExclamationTriangleIconWarning = styled(ExclamationTriangleIcon)` - color: var(--pf-global--warning-color--100); - margin-left: 18px; -`; - -function ProjectJobTemplateListItem({ - template, - isSelected, - onSelect, - detailUrl, - rowIndex, -}) { - const canLaunch = template.summary_fields.user_capabilities.start; - - const missingResourceIcon = - template.type === 'job_template' && - (!template.summary_fields.project || - (!template.summary_fields.inventory && - !template.ask_inventory_on_launch)); - - const missingExecutionEnvironment = - template.type === 'job_template' && - template.custom_virtualenv && - !template.execution_environment; - - return ( - - - - - {template.name} - {missingResourceIcon && ( - - - - )} - {missingExecutionEnvironment && ( - - - - )} - - - {toTitleCase(template.type)} - - - - - - - {({ handleLaunch, isLaunching }) => ( - - )} - - - - - - - - ); -} - -export { ProjectJobTemplateListItem as _ProjectJobTemplateListItem }; -export default ProjectJobTemplateListItem; diff --git a/awx/ui/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.test.js b/awx/ui/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.test.js deleted file mode 100644 index d61ed8f720..0000000000 --- a/awx/ui/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.test.js +++ /dev/null @@ -1,262 +0,0 @@ -import React from 'react'; - -import { createMemoryHistory } from 'history'; -import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; -import ProjectJobTemplatesListItem from './ProjectJobTemplatesListItem'; - -describe('', () => { - test('launch button shown to users with start capabilities', () => { - const wrapper = mountWithContexts( - - - - -
- ); - expect(wrapper.find('LaunchButton').exists()).toBeTruthy(); - }); - - test('launch button hidden from users without start capabilities', () => { - const wrapper = mountWithContexts( - - - - -
- ); - expect(wrapper.find('LaunchButton').exists()).toBeFalsy(); - }); - - test('edit button shown to users with edit capabilities', () => { - const wrapper = mountWithContexts( - - - - -
- ); - expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy(); - }); - - test('edit button hidden from users without edit capabilities', () => { - const wrapper = mountWithContexts( - - - - -
- ); - expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); - }); - - test('missing resource icon is shown.', () => { - const wrapper = mountWithContexts( - - - - -
- ); - expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(true); - }); - - test('missing resource icon is not shown when there is a project and an inventory.', () => { - const wrapper = mountWithContexts( - - - - -
- ); - expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false); - }); - - test('missing resource icon is not shown when inventory is prompt_on_launch, and a project', () => { - const wrapper = mountWithContexts( - - - - -
- ); - expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false); - }); - test('missing resource icon is not shown type is workflow_job_template', () => { - const wrapper = mountWithContexts( - - - - -
- ); - expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false); - }); - test('clicking on template from project templates list navigates properly', () => { - const history = createMemoryHistory({ - initialEntries: ['/projects/1/job_templates'], - }); - const wrapper = mountWithContexts( - - - - -
, - { context: { router: { history } } } - ); - wrapper.find('Link').simulate('click', { button: 0 }); - expect(history.location.pathname).toEqual( - '/templates/job_template/2/details' - ); - }); - - test('should render warning about missing execution environment', () => { - const wrapper = mountWithContexts( - - - - -
- ); - - expect( - wrapper.find('.missing-execution-environment').prop('content') - ).toEqual( - 'Custom virtual environment /var/lib/awx/env must be replaced by an execution environment.' - ); - }); -}); diff --git a/awx/ui/src/screens/Project/ProjectJobTemplatesList/index.js b/awx/ui/src/screens/Project/ProjectJobTemplatesList/index.js deleted file mode 100644 index d0be30040f..0000000000 --- a/awx/ui/src/screens/Project/ProjectJobTemplatesList/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './ProjectJobTemplatesList';