diff --git a/awx/ui_next/src/components/CopyButton/CopyButton.jsx b/awx/ui_next/src/components/CopyButton/CopyButton.jsx index fe4580f4bf..30927f22c5 100644 --- a/awx/ui_next/src/components/CopyButton/CopyButton.jsx +++ b/awx/ui_next/src/components/CopyButton/CopyButton.jsx @@ -10,12 +10,13 @@ import AlertModal from '../AlertModal'; import ErrorDetail from '../ErrorDetail'; function CopyButton({ - i18n, + id, copyItem, isDisabled, onCopyStart, onCopyFinish, helperText, + i18n, }) { const { isLoading, error: copyError, request: copyItemToAPI } = useRequest( copyItem @@ -34,6 +35,7 @@ function CopyButton({ <> + + + + {({ handleLaunch }) => ( + + )} + + + + + + + + + + + + + + + + } + dataCy={`template-${template.id}-activity`} + /> + {summaryFields.credentials && summaryFields.credentials.length && ( + + {summaryFields.credentials.map(c => ( + + ))} + + } + dataCy={`template-${template.id}-credentials`} + /> + )} + {summaryFields.inventory ? ( + + ) : ( + !askInventoryOnLaunch && ( + + ) + )} + {summaryFields.labels && summaryFields.labels.results.length > 0 && ( + + {summaryFields.labels.results.map(l => ( + + {l.name} + + ))} + + } + dataCy={`template-${template.id}-labels`} + /> + )} + {summaryFields.project && ( + + {summaryFields.project.name} + + } + dataCy={`template-${template.id}-project`} + /> + )} + + + + + + + ); +} + +export { TemplateListItem as _TemplateListItem }; +export default withI18n()(TemplateListItem); diff --git a/awx/ui_next/src/components/TemplateList/TemplateListItem.test.jsx b/awx/ui_next/src/components/TemplateList/TemplateListItem.test.jsx new file mode 100644 index 0000000000..d9726196b2 --- /dev/null +++ b/awx/ui_next/src/components/TemplateList/TemplateListItem.test.jsx @@ -0,0 +1,323 @@ +import React from 'react'; + +import { createMemoryHistory } from 'history'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import { JobTemplatesAPI } from '../../api'; +import mockJobTemplateData from './data.job_template.json'; +import TemplateListItem from './TemplateListItem'; + +jest.mock('../../api'); + +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 templates list navigates properly', () => { + const history = createMemoryHistory({ + initialEntries: ['/templates'], + }); + const wrapper = mountWithContexts( + + + + +
, + { context: { router: { history } } } + ); + wrapper.find('Link').simulate('click', { button: 0 }); + expect(history.location.pathname).toEqual( + '/templates/job_template/1/details' + ); + }); + test('should call api to copy template', async () => { + JobTemplatesAPI.copy.mockResolvedValue(); + + const wrapper = mountWithContexts( + + + + +
+ ); + await act(async () => + wrapper.find('Button[aria-label="Copy"]').prop('onClick')() + ); + expect(JobTemplatesAPI.copy).toHaveBeenCalled(); + jest.clearAllMocks(); + }); + + test('should render proper alert modal on copy error', async () => { + JobTemplatesAPI.copy.mockRejectedValue(new Error()); + + const wrapper = mountWithContexts( + + + + +
+ ); + 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( + + + + +
+ ); + expect(wrapper.find('CopyButton').length).toBe(0); + }); + + test('should render visualizer button for workflow', async () => { + const wrapper = mountWithContexts( + + + + +
+ ); + expect(wrapper.find('ProjectDiagramIcon').length).toBe(1); + }); + + test('should not render visualizer button for job template', async () => { + const wrapper = mountWithContexts( + + + + +
+ ); + expect(wrapper.find('ProjectDiagramIcon').length).toBe(0); + }); +}); diff --git a/awx/ui_next/src/components/TemplateList/data.job_template.json b/awx/ui_next/src/components/TemplateList/data.job_template.json new file mode 100644 index 0000000000..804c3b72a2 --- /dev/null +++ b/awx/ui_next/src/components/TemplateList/data.job_template.json @@ -0,0 +1,181 @@ +{ + "id": 7, + "type": "job_template", + "url": "/api/v2/job_templates/7/", + "related": { + "named_url": "/api/v2/job_templates/Mike's JT/", + "created_by": "/api/v2/users/1/", + "modified_by": "/api/v2/users/1/", + "labels": "/api/v2/job_templates/7/labels/", + "inventory": "/api/v2/inventories/1/", + "project": "/api/v2/projects/6/", + "credentials": "/api/v2/job_templates/7/credentials/", + "last_job": "/api/v2/jobs/12/", + "jobs": "/api/v2/job_templates/7/jobs/", + "schedules": "/api/v2/job_templates/7/schedules/", + "activity_stream": "/api/v2/job_templates/7/activity_stream/", + "launch": "/api/v2/job_templates/7/launch/", + "notification_templates_started": "/api/v2/job_templates/7/notification_templates_started/", + "notification_templates_success": "/api/v2/job_templates/7/notification_templates_success/", + "notification_templates_error": "/api/v2/job_templates/7/notification_templates_error/", + "access_list": "/api/v2/job_templates/7/access_list/", + "survey_spec": "/api/v2/job_templates/7/survey_spec/", + "object_roles": "/api/v2/job_templates/7/object_roles/", + "instance_groups": "/api/v2/job_templates/7/instance_groups/", + "slice_workflow_jobs": "/api/v2/job_templates/7/slice_workflow_jobs/", + "copy": "/api/v2/job_templates/7/copy/", + "webhook_receiver": "/api/v2/job_templates/7/github/", + "webhook_key": "/api/v2/job_templates/7/webhook_key/" + }, + "summary_fields": { + "inventory": { + "id": 1, + "name": "Mike's Inventory", + "description": "", + "has_active_failures": false, + "total_hosts": 1, + "hosts_with_active_failures": 0, + "total_groups": 0, + "groups_with_active_failures": 0, + "has_inventory_sources": false, + "total_inventory_sources": 0, + "inventory_sources_with_failures": 0, + "organization_id": 1, + "kind": "" + }, + "project": { + "id": 6, + "name": "Mike's Project", + "description": "", + "status": "successful", + "scm_type": "git" + }, + "last_job": { + "id": 12, + "name": "Mike's JT", + "description": "", + "finished": "2019-10-01T14:34:35.142483Z", + "status": "successful", + "failed": false + }, + "last_update": { + "id": 12, + "name": "Mike's JT", + "description": "", + "status": "successful", + "failed": false + }, + "created_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "modified_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "object_roles": { + "admin_role": { + "description": "Can manage all aspects of the job template", + "name": "Admin", + "id": 24 + }, + "execute_role": { + "description": "May run the job template", + "name": "Execute", + "id": 25 + }, + "read_role": { + "description": "May view settings for the job template", + "name": "Read", + "id": 26 + } + }, + "user_capabilities": { + "edit": true, + "delete": true, + "start": true, + "schedule": true, + "copy": true + }, + "labels": { + "count": 1, + "results": [{ + "id": 91, + "name": "L_91o2" + }] + }, + "survey": { + "title": "", + "description": "" + }, + "recent_jobs": [{ + "id": 12, + "status": "successful", + "finished": "2019-10-01T14:34:35.142483Z", + "type": "job" + }], + "credentials": [{ + "id": 1, + "kind": "ssh", + "name": "Credential 1" + }, + { + "id": 2, + "kind": "awx", + "name": "Credential 2" + } + ], + "webhook_credential": { + "id": "1", + "name": "Webhook Credential" + + } + }, + "created": "2019-09-30T16:18:34.564820Z", + "modified": "2019-10-01T14:47:31.818431Z", + "name": "Mike's JT", + "description": "", + "job_type": "run", + "inventory": 1, + "project": 6, + "playbook": "ping.yml", + "scm_branch": "Foo branch", + "forks": 0, + "limit": "", + "verbosity": 0, + "extra_vars": "", + "job_tags": "T_100,T_200", + "force_handlers": false, + "skip_tags": "S_100,S_200", + "start_at_task": "", + "timeout": 0, + "use_fact_cache": true, + "last_job_run": "2019-10-01T14:34:35.142483Z", + "last_job_failed": false, + "next_job_run": null, + "status": "successful", + "host_config_key": "", + "ask_scm_branch_on_launch": false, + "ask_diff_mode_on_launch": false, + "ask_variables_on_launch": false, + "ask_limit_on_launch": false, + "ask_tags_on_launch": false, + "ask_skip_tags_on_launch": false, + "ask_job_type_on_launch": false, + "ask_verbosity_on_launch": false, + "ask_inventory_on_launch": false, + "ask_credential_on_launch": false, + "survey_enabled": true, + "become_enabled": false, + "diff_mode": false, + "allow_simultaneous": false, + "custom_virtualenv": null, + "job_slice_count": 1, + "webhook_credential": 1, + "webhook_key": "asertdyuhjkhgfd234567kjgfds", + "webhook_service": "github" +} diff --git a/awx/ui_next/src/screens/Template/TemplateList/index.js b/awx/ui_next/src/components/TemplateList/index.js similarity index 53% rename from awx/ui_next/src/screens/Template/TemplateList/index.js rename to awx/ui_next/src/components/TemplateList/index.js index 60759a849a..f5cdbd115c 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/index.js +++ b/awx/ui_next/src/components/TemplateList/index.js @@ -1,2 +1,2 @@ -export { default as TemplateList } from './TemplateList'; +export { default } from './TemplateList'; export { default as TemplateListItem } from './TemplateListItem'; diff --git a/awx/ui_next/src/screens/Dashboard/Dashboard.jsx b/awx/ui_next/src/screens/Dashboard/Dashboard.jsx index 714e6cf152..c6d029cb67 100644 --- a/awx/ui_next/src/screens/Dashboard/Dashboard.jsx +++ b/awx/ui_next/src/screens/Dashboard/Dashboard.jsx @@ -23,7 +23,7 @@ import JobList from '../../components/JobList'; import ContentLoading from '../../components/ContentLoading'; import LineChart from './shared/LineChart'; import Count from './shared/Count'; -import DashboardTemplateList from './shared/DashboardTemplateList'; +import TemplateList from '../../components/TemplateList'; const Counts = styled.div` display: grid; @@ -247,7 +247,9 @@ function Dashboard({ i18n }) { )} {activeTabId === 1 && } - {activeTabId === 2 && } + {activeTabId === 2 && ( + + )} diff --git a/awx/ui_next/src/screens/Dashboard/Dashboard.test.jsx b/awx/ui_next/src/screens/Dashboard/Dashboard.test.jsx index ed9ac7ff22..fbee03febf 100644 --- a/awx/ui_next/src/screens/Dashboard/Dashboard.test.jsx +++ b/awx/ui_next/src/screens/Dashboard/Dashboard.test.jsx @@ -44,7 +44,7 @@ describe('', () => { .simulate('click'); }); pageWrapper.update(); - expect(pageWrapper.find('DashboardTemplateList').length).toBe(1); + expect(pageWrapper.find('TemplateList').length).toBe(1); }); test('renders month-based/all job type chart by default', () => { diff --git a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateList.jsx b/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateList.jsx deleted file mode 100644 index 7c1761cb4f..0000000000 --- a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateList.jsx +++ /dev/null @@ -1,286 +0,0 @@ -import React, { Fragment, useEffect, useState, useCallback } from 'react'; -import { useLocation, Link } from 'react-router-dom'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; -import { Card, DropdownItem } from '@patternfly/react-core'; - -import { - JobTemplatesAPI, - UnifiedJobTemplatesAPI, - WorkflowJobTemplatesAPI, -} from '../../../api'; -import AlertModal from '../../../components/AlertModal'; -import DatalistToolbar from '../../../components/DataListToolbar'; -import ErrorDetail from '../../../components/ErrorDetail'; -import PaginatedDataList, { - ToolbarDeleteButton, -} from '../../../components/PaginatedDataList'; -import useRequest, { useDeleteItems } from '../../../util/useRequest'; -import { getQSConfig, parseQueryString } from '../../../util/qs'; -import useWsTemplates from '../../../util/useWsTemplates'; -import AddDropDownButton from '../../../components/AddDropDownButton'; - -import DashboardTemplateListItem from './DashboardTemplateListItem'; - -const QS_CONFIG = getQSConfig( - 'template', - { - page: 1, - page_size: 5, - order_by: 'name', - type: 'job_template,workflow_job_template', - }, - ['id', 'page', 'page_size'] -); - -function DashboardTemplateList({ i18n }) { - // The type value in const QS_CONFIG below does not have a space between job_template and - // workflow_job_template so the params sent to the API match what the api expects. - - const location = useLocation(); - - const [selected, setSelected] = useState([]); - - const { - result: { - results, - count, - jtActions, - wfjtActions, - relatedSearchableKeys, - searchableKeys, - }, - error: contentError, - isLoading, - request: fetchTemplates, - } = useRequest( - useCallback(async () => { - const params = parseQueryString(QS_CONFIG, location.search); - const responses = await Promise.all([ - UnifiedJobTemplatesAPI.read(params), - JobTemplatesAPI.readOptions(), - WorkflowJobTemplatesAPI.readOptions(), - UnifiedJobTemplatesAPI.readOptions(), - ]); - return { - results: responses[0].data.results, - count: responses[0].data.count, - jtActions: responses[1].data.actions, - wfjtActions: responses[2].data.actions, - relatedSearchableKeys: ( - responses[3]?.data?.related_search_fields || [] - ).map(val => val.slice(0, -8)), - searchableKeys: Object.keys( - responses[3].data.actions?.GET || {} - ).filter(key => responses[3].data.actions?.GET[key].filterable), - }; - }, [location]), - { - results: [], - count: 0, - jtActions: {}, - wfjtActions: {}, - relatedSearchableKeys: [], - searchableKeys: [], - } - ); - - useEffect(() => { - fetchTemplates(); - }, [fetchTemplates]); - - const templates = useWsTemplates(results); - - const isAllSelected = - selected.length === templates.length && selected.length > 0; - const { - isLoading: isDeleteLoading, - deleteItems: deleteTemplates, - deletionError, - clearDeletionError, - } = useDeleteItems( - useCallback(() => { - return Promise.all( - selected.map(({ type, id }) => { - if (type === 'job_template') { - return JobTemplatesAPI.destroy(id); - } - if (type === 'workflow_job_template') { - return WorkflowJobTemplatesAPI.destroy(id); - } - return false; - }) - ); - }, [selected]), - { - qsConfig: QS_CONFIG, - allItemsSelected: isAllSelected, - fetchItems: fetchTemplates, - } - ); - - const handleTemplateDelete = async () => { - await deleteTemplates(); - setSelected([]); - }; - - const handleSelectAll = isSelected => { - setSelected(isSelected ? [...templates] : []); - }; - - const handleSelect = template => { - if (selected.some(s => s.id === template.id)) { - setSelected(selected.filter(s => s.id !== template.id)); - } else { - setSelected(selected.concat(template)); - } - }; - - const canAddJT = - jtActions && Object.prototype.hasOwnProperty.call(jtActions, 'POST'); - const canAddWFJT = - wfjtActions && Object.prototype.hasOwnProperty.call(wfjtActions, 'POST'); - - const addTemplate = i18n._(t`Add job template`); - const addWFTemplate = i18n._(t`Add workflow template`); - const addButton = ( - - {addTemplate} - , - - {addWFTemplate} - , - ]} - /> - ); - - return ( - - - ( - , - ]} - /> - )} - renderItem={template => ( - handleSelect(template)} - isSelected={selected.some(row => row.id === template.id)} - fetchTemplates={fetchTemplates} - /> - )} - emptyStateControls={(canAddJT || canAddWFJT) && addButton} - /> - - - {i18n._(t`Failed to delete one or more templates.`)} - - - - ); -} - -export default withI18n()(DashboardTemplateList); diff --git a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateList.test.jsx b/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateList.test.jsx deleted file mode 100644 index 5b33d3d717..0000000000 --- a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateList.test.jsx +++ /dev/null @@ -1,336 +0,0 @@ -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { - JobTemplatesAPI, - UnifiedJobTemplatesAPI, - WorkflowJobTemplatesAPI, -} from '../../../api'; -import { - mountWithContexts, - waitForElement, -} from '../../../../testUtils/enzymeHelpers'; - -import DashboardTemplateList from './DashboardTemplateList'; - -jest.mock('../../../api'); - -const mockTemplates = [ - { - id: 1, - name: 'Job Template 1', - url: '/templates/job_template/1', - type: 'job_template', - summary_fields: { - user_capabilities: { - delete: true, - edit: true, - copy: true, - }, - }, - }, - { - id: 2, - name: 'Job Template 2', - url: '/templates/job_template/2', - type: 'job_template', - summary_fields: { - user_capabilities: { - delete: true, - }, - }, - }, - { - id: 3, - name: 'Job Template 3', - url: '/templates/job_template/3', - type: 'job_template', - summary_fields: { - user_capabilities: { - delete: true, - }, - }, - }, - { - id: 4, - name: 'Workflow Job Template 1', - url: '/templates/workflow_job_template/4', - type: 'workflow_job_template', - summary_fields: { - user_capabilities: { - delete: true, - }, - }, - }, - { - id: 5, - name: 'Workflow Job Template 2', - url: '/templates/workflow_job_template/5', - type: 'workflow_job_template', - summary_fields: { - user_capabilities: { - delete: false, - }, - }, - }, -]; - -describe('', () => { - let debug; - beforeEach(() => { - UnifiedJobTemplatesAPI.read.mockResolvedValue({ - data: { - count: mockTemplates.length, - results: mockTemplates, - }, - }); - - UnifiedJobTemplatesAPI.readOptions.mockResolvedValue({ - data: { - actions: [], - }, - }); - debug = global.console.debug; // eslint-disable-line prefer-destructuring - global.console.debug = () => {}; - }); - - afterEach(() => { - jest.clearAllMocks(); - global.console.debug = debug; - }); - - test('initially renders successfully', async () => { - await act(async () => { - mountWithContexts( - - ); - }); - }); - - test('Templates are retrieved from the api and the components finishes loading', async () => { - let wrapper; - await act(async () => { - wrapper = mountWithContexts(); - }); - expect(UnifiedJobTemplatesAPI.read).toBeCalled(); - await act(async () => { - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - }); - expect(wrapper.find('DashboardTemplateListItem').length).toEqual(5); - }); - - 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('DashboardTemplateListItem') - .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('DashboardTemplateListItem') - .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('DashboardTemplateListItem') - .at(0) - .find('input'); - const nonDeleteableItem = wrapper - .find('DashboardTemplateListItem') - .at(4) - .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('DashboardTemplateListItem') - .at(1) - .find('input'); - const workflowJobTemplate = wrapper - .find('DashboardTemplateListItem') - .at(3) - .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 } }, - }, - }); - - workflowJobTemplate.simulate('change', { - target: { - id: 4, - name: 'Workflow Job Template 1', - url: '/templates/workflow_job_template/4', - type: 'workflow_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); - expect(WorkflowJobTemplatesAPI.destroy).toBeCalledWith(4); - }); - - 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', - }, - }) - ); - const wrapper = mountWithContexts(); - await act(async () => { - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - }); - const checkBox = wrapper - .find('DashboardTemplateListItem') - .at(1) - .find('input'); - - checkBox.simulate('change', { - target: { - id: 'a', - 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')(); - }); - - await waitForElement( - wrapper, - 'Modal[aria-label="Deletion Error"]', - el => el.props().isOpen === true && el.props().title === '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(); - expect(UnifiedJobTemplatesAPI.read).toHaveBeenCalled(); - wrapper.update(); - }); -}); diff --git a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateListItem.jsx b/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateListItem.jsx deleted file mode 100644 index e38af7c982..0000000000 --- a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateListItem.jsx +++ /dev/null @@ -1,182 +0,0 @@ -import 'styled-components/macro'; -import React, { useState, useCallback } from 'react'; -import { Link } from 'react-router-dom'; -import { - Button, - DataListAction as _DataListAction, - DataListCheck, - DataListItem, - DataListItemRow, - DataListItemCells, - Tooltip, -} from '@patternfly/react-core'; -import { t } from '@lingui/macro'; -import { withI18n } from '@lingui/react'; -import { - ExclamationTriangleIcon, - PencilAltIcon, - ProjectDiagramIcon, - RocketIcon, -} from '@patternfly/react-icons'; -import styled from 'styled-components'; - -import DataListCell from '../../../components/DataListCell'; -import { timeOfDay } from '../../../util/dates'; -import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../../api'; -import LaunchButton from '../../../components/LaunchButton'; -import Sparkline from '../../../components/Sparkline'; -import { toTitleCase } from '../../../util/strings'; -import CopyButton from '../../../components/CopyButton'; - -const DataListAction = styled(_DataListAction)` - align-items: center; - display: grid; - grid-gap: 16px; - grid-template-columns: repeat(4, 40px); -`; - -function DashboardTemplateListItem({ - i18n, - template, - isSelected, - onSelect, - detailUrl, - fetchTemplates, -}) { - const [isDisabled, setIsDisabled] = useState(false); - const labelId = `check-action-${template.id}`; - - const copyTemplate = useCallback(async () => { - if (template.type === 'job_template') { - await JobTemplatesAPI.copy(template.id, { - name: `${template.name} @ ${timeOfDay()}`, - }); - } else { - await WorkflowJobTemplatesAPI.copy(template.id, { - name: `${template.name} @ ${timeOfDay()}`, - }); - } - await fetchTemplates(); - }, [fetchTemplates, template.id, template.name, template.type]); - - const handleCopyStart = useCallback(() => { - setIsDisabled(true); - }, []); - - const handleCopyFinish = useCallback(() => { - setIsDisabled(false); - }, []); - - const missingResourceIcon = - template.type === 'job_template' && - (!template.summary_fields.project || - (!template.summary_fields.inventory && - !template.ask_inventory_on_launch)); - return ( - - - - - - - {template.name} - - - {missingResourceIcon && ( - - - - - - )} - , - - {toTitleCase(template.type)} - , - - - , - ]} - /> - - {template.type === 'workflow_job_template' && ( - - - - )} - {template.summary_fields.user_capabilities.start && ( - - - {({ handleLaunch }) => ( - - )} - - - )} - {template.summary_fields.user_capabilities.edit && ( - - - - )} - {template.summary_fields.user_capabilities.copy && ( - - )} - - - - ); -} - -export { DashboardTemplateListItem as _TemplateListItem }; -export default withI18n()(DashboardTemplateListItem); diff --git a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateListItem.test.jsx b/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateListItem.test.jsx deleted file mode 100644 index 571ef260c0..0000000000 --- a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateListItem.test.jsx +++ /dev/null @@ -1,268 +0,0 @@ -import React from 'react'; -import { createMemoryHistory } from 'history'; -import { act } from 'react-dom/test-utils'; - -import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; -import { JobTemplatesAPI } from '../../../api'; - -import mockJobTemplateData from './data.job_template.json'; -import DashboardTemplateListItem from './DashboardTemplateListItem'; - -jest.mock('../../../api'); - -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 templates list navigates properly', () => { - const history = createMemoryHistory({ - initialEntries: ['/templates'], - }); - const wrapper = mountWithContexts( - , - { context: { router: { history } } } - ); - wrapper.find('Link').simulate('click', { button: 0 }); - expect(history.location.pathname).toEqual( - '/templates/job_template/1/details' - ); - }); - test('should call api to copy template', async () => { - JobTemplatesAPI.copy.mockResolvedValue(); - - const wrapper = mountWithContexts( - - ); - await act(async () => - wrapper.find('Button[aria-label="Copy"]').prop('onClick')() - ); - expect(JobTemplatesAPI.copy).toHaveBeenCalled(); - jest.clearAllMocks(); - }); - - test('should render proper alert modal on copy error', async () => { - JobTemplatesAPI.copy.mockRejectedValue(new Error()); - - const wrapper = mountWithContexts( - - ); - 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( - - ); - expect(wrapper.find('CopyButton').length).toBe(0); - }); - - test('should render visualizer button for workflow', async () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.find('ProjectDiagramIcon').length).toBe(1); - }); - - test('should not render visualizer button for job template', async () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.find('ProjectDiagramIcon').length).toBe(0); - }); -}); diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx deleted file mode 100644 index 057178f656..0000000000 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx +++ /dev/null @@ -1,183 +0,0 @@ -import 'styled-components/macro'; -import React, { useState, useCallback } from 'react'; -import { Link } from 'react-router-dom'; -import { - Button, - DataListAction as _DataListAction, - DataListCheck, - DataListItem, - DataListItemRow, - DataListItemCells, - Tooltip, -} from '@patternfly/react-core'; -import { t } from '@lingui/macro'; -import { withI18n } from '@lingui/react'; -import { - ExclamationTriangleIcon, - PencilAltIcon, - ProjectDiagramIcon, - RocketIcon, -} from '@patternfly/react-icons'; -import styled from 'styled-components'; -import DataListCell from '../../../components/DataListCell'; - -import { timeOfDay } from '../../../util/dates'; - -import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../../api'; -import LaunchButton from '../../../components/LaunchButton'; -import Sparkline from '../../../components/Sparkline'; -import { toTitleCase } from '../../../util/strings'; -import CopyButton from '../../../components/CopyButton'; - -const DataListAction = styled(_DataListAction)` - align-items: center; - display: grid; - grid-gap: 16px; - grid-template-columns: repeat(4, 40px); -`; - -function TemplateListItem({ - i18n, - template, - isSelected, - onSelect, - detailUrl, - fetchTemplates, -}) { - const [isDisabled, setIsDisabled] = useState(false); - const labelId = `check-action-${template.id}`; - - const copyTemplate = useCallback(async () => { - if (template.type === 'job_template') { - await JobTemplatesAPI.copy(template.id, { - name: `${template.name} @ ${timeOfDay()}`, - }); - } else { - await WorkflowJobTemplatesAPI.copy(template.id, { - name: `${template.name} @ ${timeOfDay()}`, - }); - } - await fetchTemplates(); - }, [fetchTemplates, template.id, template.name, template.type]); - - const handleCopyStart = useCallback(() => { - setIsDisabled(true); - }, []); - - const handleCopyFinish = useCallback(() => { - setIsDisabled(false); - }, []); - - const missingResourceIcon = - template.type === 'job_template' && - (!template.summary_fields.project || - (!template.summary_fields.inventory && - !template.ask_inventory_on_launch)); - return ( - - - - - - - {template.name} - - - {missingResourceIcon && ( - - - - - - )} - , - - {toTitleCase(template.type)} - , - - - , - ]} - /> - - {template.type === 'workflow_job_template' && ( - - - - )} - {template.summary_fields.user_capabilities.start && ( - - - {({ handleLaunch }) => ( - - )} - - - )} - {template.summary_fields.user_capabilities.edit && ( - - - - )} - {template.summary_fields.user_capabilities.copy && ( - - )} - - - - ); -} - -export { TemplateListItem as _TemplateListItem }; -export default withI18n()(TemplateListItem); diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.test.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.test.jsx deleted file mode 100644 index 3c02c82672..0000000000 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.test.jsx +++ /dev/null @@ -1,267 +0,0 @@ -import React from 'react'; - -import { createMemoryHistory } from 'history'; -import { act } from 'react-dom/test-utils'; -import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; -import { JobTemplatesAPI } from '../../../api'; -import mockJobTemplateData from '../shared/data.job_template.json'; -import TemplateListItem from './TemplateListItem'; - -jest.mock('../../../api'); - -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 templates list navigates properly', () => { - const history = createMemoryHistory({ - initialEntries: ['/templates'], - }); - const wrapper = mountWithContexts( - , - { context: { router: { history } } } - ); - wrapper.find('Link').simulate('click', { button: 0 }); - expect(history.location.pathname).toEqual( - '/templates/job_template/1/details' - ); - }); - test('should call api to copy template', async () => { - JobTemplatesAPI.copy.mockResolvedValue(); - - const wrapper = mountWithContexts( - - ); - await act(async () => - wrapper.find('Button[aria-label="Copy"]').prop('onClick')() - ); - expect(JobTemplatesAPI.copy).toHaveBeenCalled(); - jest.clearAllMocks(); - }); - - test('should render proper alert modal on copy error', async () => { - JobTemplatesAPI.copy.mockRejectedValue(new Error()); - - const wrapper = mountWithContexts( - - ); - 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( - - ); - expect(wrapper.find('CopyButton').length).toBe(0); - }); - - test('should render visualizer button for workflow', async () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.find('ProjectDiagramIcon').length).toBe(1); - }); - - test('should not render visualizer button for job template', async () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.find('ProjectDiagramIcon').length).toBe(0); - }); -}); diff --git a/awx/ui_next/src/screens/Template/Templates.jsx b/awx/ui_next/src/screens/Template/Templates.jsx index f3905608cc..c5c5e1335f 100644 --- a/awx/ui_next/src/screens/Template/Templates.jsx +++ b/awx/ui_next/src/screens/Template/Templates.jsx @@ -5,7 +5,7 @@ import { Route, withRouter, Switch } from 'react-router-dom'; import { PageSection } from '@patternfly/react-core'; import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; -import { TemplateList } from './TemplateList'; +import TemplateList from '../../components/TemplateList'; import Template from './Template'; import WorkflowJobTemplate from './WorkflowJobTemplate'; import JobTemplateAdd from './JobTemplateAdd';