diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplates/ProjectJobTemplates.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplates/ProjectJobTemplates.jsx
index b80f1a4c61..72b9af7989 100644
--- a/awx/ui_next/src/screens/Project/ProjectJobTemplates/ProjectJobTemplates.jsx
+++ b/awx/ui_next/src/screens/Project/ProjectJobTemplates/ProjectJobTemplates.jsx
@@ -1,10 +1,9 @@
-import React, { Component } from 'react';
-import { CardBody } from '@components/Card';
+import React from 'react';
+import { withRouter } from 'react-router-dom';
+import TemplateList from '../../Template/TemplateList/TemplateList';
-class ProjectJobTemplates extends Component {
- render() {
- return Coming soon :);
- }
+function ProjectJobTemplates() {
+ return ;
}
-export default ProjectJobTemplates;
+export default withRouter(ProjectJobTemplates);
diff --git a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx
index f55d25bbdd..7aecba1eb2 100644
--- a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx
+++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
-import { Card, PageSection } from '@patternfly/react-core';
+import { Card } from '@patternfly/react-core';
import { CardBody } from '@components/Card';
import JobTemplateForm from '../shared/JobTemplateForm';
import { JobTemplatesAPI } from '@api';
@@ -61,17 +61,15 @@ function JobTemplateAdd() {
}
return (
-
-
-
-
-
- {formSubmitError ? formSubmitError
: ''}
-
-
+
+
+
+
+ {formSubmitError ? formSubmitError
: ''}
+
);
}
diff --git a/awx/ui_next/src/screens/Template/Template.jsx b/awx/ui_next/src/screens/Template/Template.jsx
index 00e052e101..8e22494524 100644
--- a/awx/ui_next/src/screens/Template/Template.jsx
+++ b/awx/ui_next/src/screens/Template/Template.jsx
@@ -150,78 +150,76 @@ class Template extends Component {
}
return (
-
-
- {cardHeader}
-
-
- {template && (
- (
-
- )}
- />
- )}
- {template && (
- }
- />
- )}
- {template && (
- (
-
- )}
- />
- )}
- {canSeeNotificationsTab && (
- (
-
- )}
- />
- )}
+
+ {cardHeader}
+
+
+ {template && (
- !hasContentLoading && (
-
- {match.params.id && (
-
- {i18n._(`View Template Details`)}
-
- )}
-
- )
- }
+ key="details"
+ path="/templates/:templateType/:id/details"
+ render={() => (
+
+ )}
/>
-
-
-
+ )}
+ {template && (
+ }
+ />
+ )}
+ {template && (
+ (
+
+ )}
+ />
+ )}
+ {canSeeNotificationsTab && (
+ (
+
+ )}
+ />
+ )}
+
+ !hasContentLoading && (
+
+ {match.params.id && (
+
+ {i18n._(`View Template Details`)}
+
+ )}
+
+ )
+ }
+ />
+
+
);
}
}
diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx
index 8c99916999..312c8e0fd4 100644
--- a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx
+++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx
@@ -1,8 +1,8 @@
-import React, { Component } from 'react';
-import { withRouter } from 'react-router-dom';
+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, PageSection } from '@patternfly/react-core';
+import { Card } from '@patternfly/react-core';
import {
JobTemplatesAPI,
@@ -29,65 +29,96 @@ const QS_CONFIG = getQSConfig('template', {
type: 'job_template,workflow_job_template',
});
-class TemplatesList extends Component {
- constructor(props) {
- super(props);
+function TemplatesList({ i18n }) {
+ const { id: projectId } = useParams();
+ const { pathname, search } = useLocation();
- this.state = {
- hasContentLoading: true,
- contentError: null,
- deletionError: null,
- selected: [],
- templates: [],
- itemCount: 0,
- };
+ 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([]);
- this.loadTemplates = this.loadTemplates.bind(this);
- this.handleSelectAll = this.handleSelectAll.bind(this);
- this.handleSelect = this.handleSelect.bind(this);
- this.handleTemplateDelete = this.handleTemplateDelete.bind(this);
- this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
- }
+ useEffect(
+ () => {
+ const loadTemplates = async () => {
+ const params = {
+ ...parseQueryString(QS_CONFIG, search),
+ };
- componentDidMount() {
- this.loadTemplates();
- }
+ let jtOptionsPromise;
+ if (jtActions) {
+ jtOptionsPromise = Promise.resolve({
+ data: { actions: jtActions },
+ });
+ } else {
+ jtOptionsPromise = JobTemplatesAPI.readOptions();
+ }
- componentDidUpdate(prevProps) {
- const { location } = this.props;
+ let wfjtOptionsPromise;
+ if (wfjtActions) {
+ wfjtOptionsPromise = Promise.resolve({
+ data: { actions: wfjtActions },
+ });
+ } else {
+ wfjtOptionsPromise = WorkflowJobTemplatesAPI.readOptions();
+ }
+ if (pathname.startsWith('/projects') && projectId) {
+ params.jobtemplate__project = projectId;
+ }
- if (location !== prevProps.location) {
- this.loadTemplates();
- }
- }
+ const promises = Promise.all([
+ UnifiedJobTemplatesAPI.read(params),
+ jtOptionsPromise,
+ wfjtOptionsPromise,
+ ]);
+ setDeletionError(null);
- componentWillUnmount() {
- document.removeEventListener('click', this.handleAddToggle, false);
- }
+ 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]
+ );
- handleDeleteErrorClose() {
- this.setState({ deletionError: null });
- }
+ const handleSelectAll = isSelected => {
+ const selectedItems = isSelected ? [...templates] : [];
+ setSelected(selectedItems);
+ };
- handleSelectAll(isSelected) {
- const { templates } = this.state;
- const selected = isSelected ? [...templates] : [];
- this.setState({ selected });
- }
-
- handleSelect(template) {
- const { selected } = this.state;
+ const handleSelect = template => {
if (selected.some(s => s.id === template.id)) {
- this.setState({ selected: selected.filter(s => s.id !== template.id) });
+ setSelected(selected.filter(s => s.id !== template.id));
} else {
- this.setState({ selected: selected.concat(template) });
+ setSelected(selected.concat(template));
}
- }
+ };
- async handleTemplateDelete() {
- const { selected, itemCount } = this.state;
-
- this.setState({ hasContentLoading: true });
+ const handleTemplateDelete = async () => {
+ setHasContentLoading(true);
try {
await Promise.all(
selected.map(({ type, id }) => {
@@ -100,202 +131,126 @@ class TemplatesList extends Component {
return deletePromise;
})
);
- this.setState({ itemCount: itemCount - selected.length });
+ setCount(count - selected.length);
} catch (err) {
- this.setState({ deletionError: err });
- } finally {
- await this.loadTemplates();
+ 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/`,
+ });
}
-
- async loadTemplates() {
- const { location } = this.props;
- const {
- jtActions: cachedJTActions,
- wfjtActions: cachedWFJTActions,
- } = this.state;
- const params = parseQueryString(QS_CONFIG, location.search);
-
- let jtOptionsPromise;
- if (cachedJTActions) {
- jtOptionsPromise = Promise.resolve({
- data: { actions: cachedJTActions },
- });
- } else {
- jtOptionsPromise = JobTemplatesAPI.readOptions();
- }
-
- let wfjtOptionsPromise;
- if (cachedWFJTActions) {
- wfjtOptionsPromise = Promise.resolve({
- data: { actions: cachedWFJTActions },
- });
- } else {
- wfjtOptionsPromise = WorkflowJobTemplatesAPI.readOptions();
- }
-
- const promises = Promise.all([
- UnifiedJobTemplatesAPI.read(params),
- jtOptionsPromise,
- wfjtOptionsPromise,
- ]);
-
- this.setState({ contentError: null, hasContentLoading: true });
-
- try {
- const [
- {
- data: { count, results },
- },
- {
- data: { actions: jtActions },
- },
- {
- data: { actions: wfjtActions },
- },
- ] = await promises;
-
- this.setState({
- jtActions,
- wfjtActions,
- itemCount: count,
- templates: results,
- selected: [],
- });
- } catch (err) {
- this.setState({ contentError: err });
- } finally {
- this.setState({ hasContentLoading: false });
- }
- }
-
- render() {
- const {
- contentError,
- hasContentLoading,
- deletionError,
- templates,
- itemCount,
- selected,
- jtActions,
- wfjtActions,
- } = this.state;
- const { match, i18n } = this.props;
- 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: `${match.url}/job_template/add/`,
- });
- }
- if (canAddWFJT) {
- addButtonOptions.push({
- label: i18n._(t`Workflow Template`),
- url: `${match.url}/workflow_job_template/add/`,
- });
- }
- const isAllSelected =
- selected.length === templates.length && selected.length > 0;
- const addButton = (
-
- );
- return (
-
-
- (
- ,
- (canAddJT || canAddWFJT) && addButton,
- ]}
- />
- )}
- renderItem={template => (
- this.handleSelect(template)}
- isSelected={selected.some(row => row.id === template.id)}
- />
- )}
- emptyStateControls={(canAddJT || canAddWFJT) && addButton}
- />
-
-
- {i18n._(t`Failed to delete one or more templates.`)}
-
-
-
- );
+ 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 { TemplatesList as _TemplatesList };
-export default withI18n()(withRouter(TemplatesList));
+export default withI18n()(TemplatesList);
diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx
index 51bb28c28b..ed32babfd3 100644
--- a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx
+++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx
@@ -59,7 +59,7 @@ const RightActionButtonCell = styled(ActionButtonCell)`
${rightStyle}
`;
-function TemplateListItem({ i18n, template, isSelected, onSelect }) {
+function TemplateListItem({ i18n, template, isSelected, onSelect, detailUrl }) {
const canLaunch = template.summary_fields.user_capabilities.start;
const missingResourceIcon =
@@ -86,7 +86,7 @@ function TemplateListItem({ i18n, template, isSelected, onSelect }) {
-
+
{template.name}
diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplatesList.test.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplatesList.test.jsx
index ab923d471d..58eefbbe5b 100644
--- a/awx/ui_next/src/screens/Template/TemplateList/TemplatesList.test.jsx
+++ b/awx/ui_next/src/screens/Template/TemplateList/TemplatesList.test.jsx
@@ -1,4 +1,7 @@
import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { createMemoryHistory } from 'history';
+import { Route } from 'react-router-dom';
import {
JobTemplatesAPI,
UnifiedJobTemplatesAPI,
@@ -6,7 +9,7 @@ import {
} from '@api';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
-import TemplatesList, { _TemplatesList } from './TemplateList';
+import TemplatesList from './TemplateList';
jest.mock('@api');
@@ -88,119 +91,181 @@ describe('', () => {
jest.clearAllMocks();
});
- test('initially renders successfully', () => {
- mountWithContexts(
-
- );
- });
-
- test('Templates are retrieved from the api and the components finishes loading', async done => {
- const loadTemplates = jest.spyOn(_TemplatesList.prototype, 'loadTemplates');
- const wrapper = mountWithContexts();
- await waitForElement(
- wrapper,
- 'TemplatesList',
- el => el.state('hasContentLoading') === true
- );
- expect(loadTemplates).toHaveBeenCalled();
- await waitForElement(
- wrapper,
- 'TemplatesList',
- el => el.state('hasContentLoading') === false
- );
- done();
- });
-
- test('handleSelect is called when a template list item is selected', async done => {
- const handleSelect = jest.spyOn(_TemplatesList.prototype, 'handleSelect');
- const wrapper = mountWithContexts();
- await waitForElement(
- wrapper,
- 'TemplatesList',
- el => el.state('hasContentLoading') === false
- );
- await wrapper
- .find('input#select-jobTemplate-1')
- .closest('DataListCheck')
- .props()
- .onChange();
- expect(handleSelect).toBeCalled();
- await waitForElement(
- wrapper,
- 'TemplatesList',
- el => el.state('selected').length === 1
- );
- done();
- });
-
- test('handleSelectAll is called when a template list item is selected', async done => {
- const handleSelectAll = jest.spyOn(
- _TemplatesList.prototype,
- 'handleSelectAll'
- );
- const wrapper = mountWithContexts();
- await waitForElement(
- wrapper,
- 'TemplatesList',
- el => el.state('hasContentLoading') === false
- );
- wrapper
- .find('Checkbox#select-all')
- .props()
- .onChange(true);
- expect(handleSelectAll).toBeCalled();
- await waitForElement(
- wrapper,
- 'TemplatesList',
- el => el.state('selected').length === 5
- );
- done();
- });
-
- test('delete button is disabled if user does not have delete capabilities on a selected template', async done => {
- const wrapper = mountWithContexts();
- wrapper.find('TemplatesList').setState({
- templates: mockTemplates,
- itemCount: 5,
- isInitialized: true,
- selected: mockTemplates.slice(0, 4),
+ test('initially renders successfully', async () => {
+ await act(async () => {
+ mountWithContexts(
+
+ );
});
- await waitForElement(
- wrapper,
- 'ToolbarDeleteButton * button',
- el => el.getDOMNode().disabled === false
- );
- wrapper.find('TemplatesList').setState({
- selected: mockTemplates,
- });
- await waitForElement(
- wrapper,
- 'ToolbarDeleteButton * button',
- el => el.getDOMNode().disabled === true
- );
- done();
});
- test('api is called to delete templates for each selected template.', () => {
- JobTemplatesAPI.destroy = jest.fn();
- WorkflowJobTemplatesAPI.destroy = jest.fn();
+ 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('TemplateListItem').length).toEqual(5);
+ });
+
+ test('handleSelect is called when a template list item is selected', async () => {
const wrapper = mountWithContexts();
- wrapper.find('TemplatesList').setState({
- templates: mockTemplates,
- itemCount: 5,
- isInitialized: true,
- isModalOpen: true,
- selected: mockTemplates.slice(0, 4),
+ await act(async () => {
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
});
- wrapper.find('ToolbarDeleteButton').prop('onDelete')();
- expect(JobTemplatesAPI.destroy).toHaveBeenCalledTimes(3);
- expect(WorkflowJobTemplatesAPI.destroy).toHaveBeenCalledTimes(1);
+ 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('error is shown when template not successfully deleted from api', async done => {
+ 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(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('TemplateListItem')
+ .at(1)
+ .find('input');
+ const workflowJobTemplate = wrapper
+ .find('TemplateListItem')
+ .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,
+ },
+ },
+ },
+ });
+
+ 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: {
@@ -213,20 +278,66 @@ describe('', () => {
})
);
const wrapper = mountWithContexts();
- wrapper.find('TemplatesList').setState({
- templates: mockTemplates,
- itemCount: 1,
- isInitialized: true,
- isModalOpen: true,
- selected: mockTemplates.slice(0, 1),
+ 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: 'a',
+ name: 'Job Template 2',
+ url: '/templates/job_template/2',
+ type: 'job_template',
+ summary_fields: { user_capabilities: { delete: true } },
+ },
+ });
+ wrapper.find('button[aria-label="Delete"]').prop('onClick')();
+ wrapper.update();
+ await act(async () => {
+ await wrapper
+ .find('button[aria-label="confirm delete"]')
+ .prop('onClick')();
});
- wrapper.find('ToolbarDeleteButton').prop('onDelete')();
await waitForElement(
wrapper,
'Modal',
el => el.props().isOpen === true && el.props().title === 'Error!'
);
-
- done();
+ });
+ test('Calls API with jobtemplate__project id', async () => {
+ const history = createMemoryHistory({
+ initialEntries: ['/projects/6/job_templates'],
+ });
+ const wrapper = mountWithContexts(
+ }
+ />,
+ {
+ context: {
+ router: {
+ history,
+ route: {
+ location: history.location,
+ match: { params: { id: 6 } },
+ },
+ },
+ },
+ }
+ );
+ await act(async () => {
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 1);
+ });
+ expect(UnifiedJobTemplatesAPI.read).toBeCalledWith({
+ jobtemplate__project: '6',
+ order_by: 'name',
+ page: 1,
+ page_size: 20,
+ type: 'job_template,workflow_job_template',
+ });
});
});
diff --git a/awx/ui_next/src/screens/Template/Templates.jsx b/awx/ui_next/src/screens/Template/Templates.jsx
index 904c7cbc3b..6b93604667 100644
--- a/awx/ui_next/src/screens/Template/Templates.jsx
+++ b/awx/ui_next/src/screens/Template/Templates.jsx
@@ -1,7 +1,8 @@
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Route, withRouter, Switch } from 'react-router-dom';
+import { PageSection } from '@patternfly/react-core';
import { Config } from '@contexts/Config';
import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs';
@@ -50,48 +51,50 @@ class Templates extends Component {
const { match, history, location } = this.props;
const { breadcrumbConfig } = this.state;
return (
-
+ <>
-
- }
- />
- (
-
- {({ me }) => (
-
- )}
-
- )}
- />
- (
-
- {({ me }) => (
-
- )}
-
- )}
- />
- } />
-
-
+
+
+ }
+ />
+ (
+
+ {({ me }) => (
+
+ )}
+
+ )}
+ />
+ (
+
+ {({ me }) => (
+
+ )}
+
+ )}
+ />
+ } />
+
+
+ >
);
}
}
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx
index be608c4e1c..96dd9683fb 100644
--- a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx
@@ -94,60 +94,58 @@ class WorkflowJobTemplate extends Component {
}
return (
-
-
- {cardHeader}
-
-
- {template && (
- (
-
- )}
- />
- )}
- {template && (
- (
-
-
-
-
-
- )}
- />
- )}
+
+ {cardHeader}
+
+
+ {template && (
- !hasContentLoading && (
-
- {match.params.id && (
-
- {i18n._(`View Template Details`)}
-
- )}
-
- )
- }
+ key="wfjt-details"
+ path="/templates/workflow_job_template/:id/details"
+ render={() => (
+
+ )}
/>
-
-
-
+ )}
+ {template && (
+ (
+
+
+
+
+
+ )}
+ />
+ )}
+
+ !hasContentLoading && (
+
+ {match.params.id && (
+
+ {i18n._(`View Template Details`)}
+
+ )}
+
+ )
+ }
+ />
+
+
);
}
}