From 9b11df04b3fa7382379e2695ef08b66a6a30b713 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Thu, 20 Feb 2020 15:16:49 -0500 Subject: [PATCH 1/4] Fixes navigation bug --- .../TemplateList/TemplateListItem.jsx | 8 +- .../TemplateList/TemplateListItem.test.jsx | 87 ++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx index b13eb92298..f08a0ccb06 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import { Button, DataListAction as _DataListAction, @@ -40,6 +40,12 @@ function TemplateListItem({ i18n, template, isSelected, onSelect, detailUrl }) { (!template.summary_fields.inventory && !template.ask_inventory_on_launch)); + const location = useLocation(); + + if (location.pathname.startsWith('/projects')) { + detailUrl = `/templates/job_template/${template.id}/details`; + } + return ( diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.test.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.test.jsx index 8fa48e08d6..d834478668 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.test.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.test.jsx @@ -1,7 +1,8 @@ import React from 'react'; +import { Route } from 'react-router-dom'; import { mountWithContexts } from '@testUtils/enzymeHelpers'; - +import { createMemoryHistory } from 'history'; import TemplateListItem from './TemplateListItem'; describe('', () => { @@ -161,4 +162,88 @@ describe('', () => { ); 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, + route: { + location: history.location, + match: { + params: { id: 1 }, + }, + }, + }, + }, + } + ); + wrapper.find('Link').simulate('click', { button: 0 }); + expect(history.location.pathname).toEqual( + '/templates/job_template/1/details' + ); + }); + test('clicking on template from project templates list navigates properly', () => { + const history = createMemoryHistory({ + initialEntries: ['/projects/1/job_templates'], + }); + const wrapper = mountWithContexts( + ( + + )} + />, + { + context: { + router: { + history, + route: { + location: history.location, + match: { + params: { id: 1 }, + }, + }, + }, + }, + } + ); + wrapper.find('Link').simulate('click', { button: 0 }); + expect(history.location.pathname).toEqual( + '/templates/job_template/2/details' + ); + }); }); From df77147d65bf13cfe3c338d10036cce9ba6b2c15 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Fri, 21 Feb 2020 12:33:53 -0500 Subject: [PATCH 2/4] WIP --- awx/ui_next/src/screens/Project/Project.jsx | 4 +- .../ProjectJobTemplates.jsx | 9 - .../Project/ProjectJobTemplates/index.js | 1 - .../ProjectJobTemplatesList.jsx | 271 ++++++++++++++++++ .../Project/ProjectJobTemplatesList/index.js | 4 + .../TemplateList/TemplateListItem.jsx | 8 +- 6 files changed, 281 insertions(+), 16 deletions(-) delete mode 100644 awx/ui_next/src/screens/Project/ProjectJobTemplates/ProjectJobTemplates.jsx delete mode 100644 awx/ui_next/src/screens/Project/ProjectJobTemplates/index.js create mode 100644 awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx create mode 100644 awx/ui_next/src/screens/Project/ProjectJobTemplatesList/index.js diff --git a/awx/ui_next/src/screens/Project/Project.jsx b/awx/ui_next/src/screens/Project/Project.jsx index a6a2cf16b0..c721e376f0 100644 --- a/awx/ui_next/src/screens/Project/Project.jsx +++ b/awx/ui_next/src/screens/Project/Project.jsx @@ -11,7 +11,7 @@ import NotificationList from '@components/NotificationList'; import { ResourceAccessList } from '@components/ResourceAccessList'; import ProjectDetail from './ProjectDetail'; import ProjectEdit from './ProjectEdit'; -import ProjectJobTemplates from './ProjectJobTemplates'; +import ProjectJobTemplatesList from './ProjectJobTemplatesList'; import ProjectSchedules from './ProjectSchedules'; import { OrganizationsAPI, ProjectsAPI } from '@api'; @@ -225,7 +225,7 @@ class Project extends Component { ( - + )} /> ; -} - -export default withRouter(ProjectJobTemplates); diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplates/index.js b/awx/ui_next/src/screens/Project/ProjectJobTemplates/index.js deleted file mode 100644 index 7652ab295d..0000000000 --- a/awx/ui_next/src/screens/Project/ProjectJobTemplates/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './ProjectJobTemplates'; diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx new file mode 100644 index 0000000000..5f36cd7938 --- /dev/null +++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx @@ -0,0 +1,271 @@ +import React, { useEffect, useState } from 'react'; +import { useParams, useLocation } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Card } 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 { getQSConfig, parseQueryString } from '@util/qs'; + +import AddDropDownButton from '@components/AddDropDownButton'; +import ProjectTemplatesListItem from '../../Template/TemplateList/TemplateListItem'; + +// 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 QS_CONFIG = getQSConfig('template', { + page: 1, + page_size: 20, + order_by: 'name', + type: 'job_template,workflow_job_template', +}); + +function ProjectJobTemplatesList({ i18n }) { + const { id: projectId } = useParams(); + const { pathname, search } = useLocation(); + + const [deletionError, setDeletionError] = useState(null); + const [contentError, setContentError] = useState(null); + const [hasContentLoading, setHasContentLoading] = useState(true); + const [jtActions, setJTActions] = useState(null); + const [wfjtActions, setWFJTActions] = useState(null); + const [count, setCount] = useState(0); + const [templates, setTemplates] = useState([]); + const [selected, setSelected] = useState([]); + + useEffect( + () => { + const loadTemplates = async () => { + const params = { + ...parseQueryString(QS_CONFIG, search), + }; + + let jtOptionsPromise; + if (jtActions) { + jtOptionsPromise = Promise.resolve({ + data: { actions: jtActions }, + }); + } else { + jtOptionsPromise = JobTemplatesAPI.readOptions(); + } + + let wfjtOptionsPromise; + if (wfjtActions) { + wfjtOptionsPromise = Promise.resolve({ + data: { actions: wfjtActions }, + }); + } else { + wfjtOptionsPromise = WorkflowJobTemplatesAPI.readOptions(); + } + if (pathname.startsWith('/projects') && projectId) { + params.jobtemplate__project = projectId; + } + + const promises = Promise.all([ + UnifiedJobTemplatesAPI.read(params), + jtOptionsPromise, + wfjtOptionsPromise, + ]); + setDeletionError(null); + + try { + const [ + { + data: { count: itemCount, results }, + }, + { + data: { actions: jobTemplateActions }, + }, + { + data: { actions: workFlowJobTemplateActions }, + }, + ] = await promises; + setJTActions(jobTemplateActions); + setWFJTActions(workFlowJobTemplateActions); + setCount(itemCount); + setTemplates(results); + setHasContentLoading(false); + } catch (err) { + setContentError(err); + } + }; + loadTemplates(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [pathname, search, count, projectId] + ); + + const handleSelectAll = isSelected => { + const selectedItems = isSelected ? [...templates] : []; + setSelected(selectedItems); + }; + + 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 handleTemplateDelete = async () => { + setHasContentLoading(true); + try { + await Promise.all( + selected.map(({ type, id }) => { + let deletePromise; + if (type === 'job_template') { + deletePromise = JobTemplatesAPI.destroy(id); + } else if (type === 'workflow_job_template') { + deletePromise = WorkflowJobTemplatesAPI.destroy(id); + } + return deletePromise; + }) + ); + setCount(count - selected.length); + } catch (err) { + setDeletionError(err); + } + }; + + const canAddJT = + jtActions && Object.prototype.hasOwnProperty.call(jtActions, 'POST'); + const canAddWFJT = + wfjtActions && Object.prototype.hasOwnProperty.call(wfjtActions, 'POST'); + const addButtonOptions = []; + if (canAddJT) { + addButtonOptions.push({ + label: i18n._(t`Template`), + url: `/templates/job_template/add/`, + }); + } + if (canAddWFJT) { + addButtonOptions.push({ + label: i18n._(t`Workflow Template`), + url: `/templates/workflow_job_template/add/`, + }); + } + const isAllSelected = + selected.length === templates.length && selected.length > 0; + const addButton = ( + + ); + return ( + <> + + ( + , + (canAddJT || canAddWFJT) && addButton, + ]} + /> + )} + renderItem={template => ( + handleSelect(template)} + isSelected={selected.some(row => row.id === template.id)} + /> + )} + emptyStateControls={(canAddJT || canAddWFJT) && addButton} + /> + + setDeletionError(null)} + > + {i18n._(t`Failed to delete one or more templates.`)} + + + + ); +} + +export default withI18n()(ProjectJobTemplatesList); diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/index.js b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/index.js new file mode 100644 index 0000000000..7aac97295e --- /dev/null +++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/index.js @@ -0,0 +1,4 @@ +export { + default +} +from './ProjectJobTemplatesList'; diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx index f08a0ccb06..86c506912e 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx @@ -40,11 +40,11 @@ function TemplateListItem({ i18n, template, isSelected, onSelect, detailUrl }) { (!template.summary_fields.inventory && !template.ask_inventory_on_launch)); - const location = useLocation(); + // const location = useLocation(); - if (location.pathname.startsWith('/projects')) { - detailUrl = `/templates/job_template/${template.id}/details`; - } + // if (location.pathname.startsWith('/projects')) { + // detailUrl = `/templates/job_template/${template.id}/details`; + // } return ( From ce8897d3e82580f344a85cf69f28a6da045d50a0 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Sun, 23 Feb 2020 11:16:44 -0500 Subject: [PATCH 3/4] Fixes naviation bug by create ProjectTemplateList and ProjectTemplateListItem Adds tests for those new files and removes uncessary test from TemplateListItem --- .../ProjectJobTemplatesList.jsx | 4 +- .../ProjectJobTemplatesListItem.jsx | 126 ++++++++++++ .../ProjectJobTemplatesListItem.test.jsx | 189 ++++++++++++++++++ .../TemplateList/TemplateListItem.jsx | 8 +- .../TemplateList/TemplateListItem.test.jsx | 84 ++------ 5 files changed, 330 insertions(+), 81 deletions(-) create mode 100644 awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx create mode 100644 awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.test.jsx diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx index 5f36cd7938..bec336f4e6 100644 --- a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx +++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx @@ -18,7 +18,7 @@ import PaginatedDataList, { import { getQSConfig, parseQueryString } from '@util/qs'; import AddDropDownButton from '@components/AddDropDownButton'; -import ProjectTemplatesListItem from '../../Template/TemplateList/TemplateListItem'; +import ProjectTemplatesListItem from './ProjectJobTemplatesListItem'; // 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. @@ -238,7 +238,7 @@ function ProjectJobTemplatesList({ i18n }) { itemsToDelete={selected} pluralizedItemName="Templates" />, - (canAddJT || canAddWFJT) && addButton, + ...(canAddJT || canAddWFJT ? [addButton] : []), ]} /> )} diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx new file mode 100644 index 0000000000..ea46bb967b --- /dev/null +++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { + Button, + DataListAction as _DataListAction, + DataListCell, + DataListCheck, + DataListItem, + DataListItemRow, + DataListItemCells, + Tooltip, +} from '@patternfly/react-core'; +import { t } from '@lingui/macro'; +import { withI18n } from '@lingui/react'; +import { + ExclamationTriangleIcon, + PencilAltIcon, + RocketIcon, +} from '@patternfly/react-icons'; + +import LaunchButton from '@components/LaunchButton'; +import Sparkline from '@components/Sparkline'; +import { toTitleCase } from '@util/strings'; +import styled from 'styled-components'; + +const DataListAction = styled(_DataListAction)` + align-items: center; + display: grid; + grid-gap: 16px; + grid-template-columns: repeat(2, 40px); +`; + +function ProjectJobTemplateListItem({ + i18n, + template, + isSelected, + onSelect, + detailUrl, +}) { + const labelId = `check-action-${template.id}`; + 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)); + + return ( + + + + + + + {template.name} + + + {missingResourceIcon && ( + + + + + + )} + , + + {toTitleCase(template.type)} + , + + + , + ]} + /> + + {canLaunch && template.type === 'job_template' && ( + + + {({ handleLaunch }) => ( + + )} + + + )} + {template.summary_fields.user_capabilities.edit && ( + + + + )} + + + + ); +} + +export { ProjectJobTemplateListItem as _ProjectJobTemplateListItem }; +export default withI18n()(ProjectJobTemplateListItem); diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.test.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.test.jsx new file mode 100644 index 0000000000..f94fc1a6e2 --- /dev/null +++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.test.jsx @@ -0,0 +1,189 @@ +import React from 'react'; + +import { mountWithContexts } from '@testUtils/enzymeHelpers'; +import { createMemoryHistory } from 'history'; +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' + ); + }); +}); diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx index 86c506912e..b13eb92298 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Link, useLocation } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { Button, DataListAction as _DataListAction, @@ -40,12 +40,6 @@ function TemplateListItem({ i18n, template, isSelected, onSelect, detailUrl }) { (!template.summary_fields.inventory && !template.ask_inventory_on_launch)); - // const location = useLocation(); - - // if (location.pathname.startsWith('/projects')) { - // detailUrl = `/templates/job_template/${template.id}/details`; - // } - return ( diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.test.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.test.jsx index d834478668..df84a79cec 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.test.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.test.jsx @@ -1,5 +1,4 @@ import React from 'react'; -import { Route } from 'react-router-dom'; import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { createMemoryHistory } from 'history'; @@ -167,83 +166,24 @@ describe('', () => { initialEntries: ['/templates'], }); const wrapper = mountWithContexts( - ( - - )} - />, - { - context: { - router: { - history, - route: { - location: history.location, - match: { - params: { id: 1 }, - }, + , + { context: { router: { history } } } ); wrapper.find('Link').simulate('click', { button: 0 }); expect(history.location.pathname).toEqual( '/templates/job_template/1/details' ); }); - test('clicking on template from project templates list navigates properly', () => { - const history = createMemoryHistory({ - initialEntries: ['/projects/1/job_templates'], - }); - const wrapper = mountWithContexts( - ( - - )} - />, - { - context: { - router: { - history, - route: { - location: history.location, - match: { - params: { id: 1 }, - }, - }, - }, - }, - } - ); - wrapper.find('Link').simulate('click', { button: 0 }); - expect(history.location.pathname).toEqual( - '/templates/job_template/2/details' - ); - }); }); From f561bf57541bd740dadfd4f3349d63e861767e83 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Mon, 24 Feb 2020 09:19:02 -0500 Subject: [PATCH 4/4] prettier --- .../src/screens/Project/ProjectJobTemplatesList/index.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/index.js b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/index.js index 7aac97295e..d0be30040f 100644 --- a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/index.js +++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/index.js @@ -1,4 +1 @@ -export { - default -} -from './ProjectJobTemplatesList'; +export { default } from './ProjectJobTemplatesList';