diff --git a/awx/ui_next/src/api/models/ExecutionEnvironments.js b/awx/ui_next/src/api/models/ExecutionEnvironments.js
index 2df933d53a..ae3d128ed3 100644
--- a/awx/ui_next/src/api/models/ExecutionEnvironments.js
+++ b/awx/ui_next/src/api/models/ExecutionEnvironments.js
@@ -5,6 +5,16 @@ class ExecutionEnvironments extends Base {
super(http);
this.baseUrl = '/api/v2/execution_environments/';
}
+
+ readUnifiedJobTemplates(id, params) {
+ return this.http.get(`${this.baseUrl}${id}/unified_job_templates/`, {
+ params,
+ });
+ }
+
+ readUnifiedJobTemplateOptions(id) {
+ return this.http.options(`${this.baseUrl}${id}/unified_job_templates/`);
+ }
}
export default ExecutionEnvironments;
diff --git a/awx/ui_next/src/api/models/Organizations.js b/awx/ui_next/src/api/models/Organizations.js
index fd980fece8..a2baa4f9c8 100644
--- a/awx/ui_next/src/api/models/Organizations.js
+++ b/awx/ui_next/src/api/models/Organizations.js
@@ -36,10 +36,8 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
});
}
- readExecutionEnvironmentsOptions(id, params) {
- return this.http.options(`${this.baseUrl}${id}/execution_environments/`, {
- params,
- });
+ readExecutionEnvironmentsOptions(id) {
+ return this.http.options(`${this.baseUrl}${id}/execution_environments/`);
}
createUser(id, data) {
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx
index 55a3228e13..bb86dc2f57 100644
--- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx
@@ -20,6 +20,7 @@ import ContentLoading from '../../components/ContentLoading';
import ExecutionEnvironmentDetails from './ExecutionEnvironmentDetails';
import ExecutionEnvironmentEdit from './ExecutionEnvironmentEdit';
+import ExecutionEnvironmentTemplateList from './ExecutionEnvironmentTemplate';
function ExecutionEnvironment({ i18n, setBreadcrumb }) {
const { id } = useParams();
@@ -64,6 +65,11 @@ function ExecutionEnvironment({ i18n, setBreadcrumb }) {
link: `/execution_environments/${id}/details`,
id: 0,
},
+ {
+ name: i18n._(t`Templates`),
+ link: `/execution_environments/${id}/templates`,
+ id: 1,
+ },
];
if (!isLoading && contentError) {
@@ -114,6 +120,11 @@ function ExecutionEnvironment({ i18n, setBreadcrumb }) {
executionEnvironment={executionEnvironment}
/>
+
+
+
>
)}
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.jsx
new file mode 100644
index 0000000000..c2b36eee30
--- /dev/null
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.jsx
@@ -0,0 +1,139 @@
+import React, { useEffect, useCallback } from 'react';
+import { useLocation } from 'react-router-dom';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { Card } from '@patternfly/react-core';
+
+import { ExecutionEnvironmentsAPI } from '../../../api';
+import { getQSConfig, parseQueryString } from '../../../util/qs';
+import useRequest from '../../../util/useRequest';
+import DatalistToolbar from '../../../components/DataListToolbar';
+import PaginatedDataList from '../../../components/PaginatedDataList';
+
+import ExecutionEnvironmentTemplateListItem from './ExecutionEnvironmentTemplateListItem';
+
+const QS_CONFIG = getQSConfig(
+ 'execution_environments',
+ {
+ page: 1,
+ page_size: 20,
+ order_by: 'name',
+ type: 'job_template,workflow_job_template',
+ },
+ ['id', 'page', 'page_size']
+);
+
+function ExecutionEnvironmentTemplateList({ i18n, executionEnvironment }) {
+ const { id } = executionEnvironment;
+ const location = useLocation();
+
+ const {
+ error: contentError,
+ isLoading,
+ request: fetchTemplates,
+ result: {
+ templates,
+ templatesCount,
+ relatedSearchableKeys,
+ searchableKeys,
+ },
+ } = useRequest(
+ useCallback(async () => {
+ const params = parseQueryString(QS_CONFIG, location.search);
+
+ const [response, responseActions] = await Promise.all([
+ ExecutionEnvironmentsAPI.readUnifiedJobTemplates(id, params),
+ ExecutionEnvironmentsAPI.readUnifiedJobTemplateOptions(id),
+ ]);
+
+ return {
+ templates: response.data.results,
+ templatesCount: response.data.count,
+ actions: responseActions.data.actions,
+ relatedSearchableKeys: (
+ responseActions?.data?.related_search_fields || []
+ ).map(val => val.slice(0, -8)),
+ searchableKeys: Object.keys(
+ responseActions.data.actions?.GET || {}
+ ).filter(key => responseActions.data.actions?.GET[key].filterable),
+ };
+ }, [location, id]),
+ {
+ templates: [],
+ templatesCount: 0,
+ actions: {},
+ relatedSearchableKeys: [],
+ searchableKeys: [],
+ }
+ );
+
+ useEffect(() => {
+ fetchTemplates();
+ }, [fetchTemplates]);
+
+ return (
+ <>
+
+ (
+
+ )}
+ renderItem={template => (
+
+ )}
+ />
+
+ >
+ );
+}
+
+export default withI18n()(ExecutionEnvironmentTemplateList);
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.test.jsx
new file mode 100644
index 0000000000..078d6d249d
--- /dev/null
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.test.jsx
@@ -0,0 +1,116 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+
+import {
+ mountWithContexts,
+ waitForElement,
+} from '../../../../testUtils/enzymeHelpers';
+
+import { ExecutionEnvironmentsAPI } from '../../../api';
+import ExecutionEnvironmentTemplateList from './ExecutionEnvironmentTemplateList';
+
+jest.mock('../../../api/');
+
+const templates = {
+ data: {
+ count: 3,
+ results: [
+ {
+ id: 1,
+ type: 'job_template',
+ name: 'Foo',
+ url: '/api/v2/job_templates/1/',
+ related: {
+ execution_environment: '/api/v2/execution_environments/1/',
+ },
+ },
+ {
+ id: 2,
+ type: 'workflow_job_template',
+ name: 'Bar',
+ url: '/api/v2/workflow_job_templates/2/',
+ related: {
+ execution_environment: '/api/v2/execution_environments/1/',
+ },
+ },
+ {
+ id: 3,
+ type: 'job_template',
+ name: 'Fuzz',
+ url: '/api/v2/job_templates/3/',
+ related: {
+ execution_environment: '/api/v2/execution_environments/1/',
+ },
+ },
+ ],
+ },
+};
+
+const mockExecutionEnvironment = {
+ id: 1,
+ name: 'Default EE',
+};
+
+const options = { data: { actions: { GET: {} } } };
+
+describe('', () => {
+ let wrapper;
+
+ test('should mount successfully', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ await waitForElement(
+ wrapper,
+ 'ExecutionEnvironmentTemplateList',
+ el => el.length > 0
+ );
+ });
+
+ test('should have data fetched and render 3 rows', async () => {
+ ExecutionEnvironmentsAPI.readUnifiedJobTemplates.mockResolvedValue(
+ templates
+ );
+
+ ExecutionEnvironmentsAPI.readUnifiedJobTemplateOptions.mockResolvedValue(
+ options
+ );
+
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ await waitForElement(
+ wrapper,
+ 'ExecutionEnvironmentTemplateList',
+ el => el.length > 0
+ );
+
+ expect(wrapper.find('ExecutionEnvironmentTemplateListItem').length).toBe(3);
+ expect(ExecutionEnvironmentsAPI.readUnifiedJobTemplates).toBeCalled();
+ expect(ExecutionEnvironmentsAPI.readUnifiedJobTemplateOptions).toBeCalled();
+ });
+
+ test('should not render add button', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ waitForElement(
+ wrapper,
+ 'ExecutionEnvironmentTemplateList',
+ el => el.length > 0
+ );
+ expect(wrapper.find('ToolbarAddButton').length).toBe(0);
+ });
+});
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.jsx
new file mode 100644
index 0000000000..4a33126386
--- /dev/null
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { Link } from 'react-router-dom';
+import {
+ DataListItem,
+ DataListItemRow,
+ DataListItemCells,
+} from '@patternfly/react-core';
+
+import DataListCell from '../../../components/DataListCell';
+
+function ExecutionEnvironmentTemplateListItem({ template, detailUrl, i18n }) {
+ return (
+
+
+
+
+ {template.name}
+
+ ,
+
+ {template.type === 'job_template'
+ ? i18n._(t`Job Template`)
+ : i18n._(t`Workflow Job Template`)}
+ ,
+ ]}
+ />
+
+
+ );
+}
+
+export default withI18n()(ExecutionEnvironmentTemplateListItem);
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.test.jsx
new file mode 100644
index 0000000000..9c107ab19b
--- /dev/null
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.test.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+
+import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
+
+import ExecutionEnvironmentTemplateListItem from './ExecutionEnvironmentTemplateListItem';
+
+describe('', () => {
+ let wrapper;
+ const template = {
+ id: 1,
+ name: 'Foo',
+ type: 'job_template',
+ };
+
+ test('should mount successfully', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ expect(wrapper.find('ExecutionEnvironmentTemplateListItem').length).toBe(1);
+ expect(wrapper.find('DataListCell[aria-label="Name"]').text()).toBe(
+ template.name
+ );
+ expect(
+ wrapper.find('DataListCell[aria-label="Template type"]').text()
+ ).toBe('Job Template');
+ });
+
+ test('should distinguish template types', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ expect(wrapper.find('ExecutionEnvironmentTemplateListItem').length).toBe(1);
+ expect(
+ wrapper.find('DataListCell[aria-label="Template type"]').text()
+ ).toBe('Workflow Job Template');
+ });
+});
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/index.js b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/index.js
new file mode 100644
index 0000000000..3bdea254ee
--- /dev/null
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/index.js
@@ -0,0 +1 @@
+export { default } from './ExecutionEnvironmentTemplateList';
diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx
index 9f2c4ae817..d567c24097 100644
--- a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx
+++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx
@@ -38,7 +38,7 @@ function OrganizationExecEnvList({ i18n, organization }) {
const [response, responseActions] = await Promise.all([
OrganizationsAPI.readExecutionEnvironments(id, params),
- OrganizationsAPI.readExecutionEnvironmentsOptions(id, params),
+ OrganizationsAPI.readExecutionEnvironmentsOptions(id),
]);
return {