diff --git a/awx/ui_next/src/api/models/Organizations.js b/awx/ui_next/src/api/models/Organizations.js
index ce236067b4b2..ec4a14c54981 100644
--- a/awx/ui_next/src/api/models/Organizations.js
+++ b/awx/ui_next/src/api/models/Organizations.js
@@ -29,6 +29,17 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
params,
});
}
+ readExecutionEnvironments(id, params) {
+ return this.http.get(`${this.baseUrl}${id}/execution_environments/`, {
+ params,
+ });
+ }
+
+ readExecutionEnvironmentsOptions(id, params) {
+ return this.http.options(`${this.baseUrl}${id}/execution_environments/`, {
+ params,
+ });
+ }
createUser(id, data) {
return this.http.post(`${this.baseUrl}${id}/users/`, data);
diff --git a/awx/ui_next/src/screens/Organization/Organization.jsx b/awx/ui_next/src/screens/Organization/Organization.jsx
index 1a2ee641c946..a826ef641d28 100644
--- a/awx/ui_next/src/screens/Organization/Organization.jsx
+++ b/awx/ui_next/src/screens/Organization/Organization.jsx
@@ -22,6 +22,7 @@ import OrganizationDetail from './OrganizationDetail';
import OrganizationEdit from './OrganizationEdit';
import OrganizationTeams from './OrganizationTeams';
import { OrganizationsAPI } from '../../api';
+import OrganizationExecEnvList from './OrganizationExecEnvList';
function Organization({ i18n, setBreadcrumb, me }) {
const location = useLocation();
@@ -122,6 +123,11 @@ function Organization({ i18n, setBreadcrumb, me }) {
{ name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 },
{ name: i18n._(t`Access`), link: `${match.url}/access`, id: 1 },
{ name: i18n._(t`Teams`), link: `${match.url}/teams`, id: 2 },
+ {
+ name: i18n._(t`Execution Environments`),
+ link: `${match.url}/execution_environments`,
+ id: 4,
+ },
];
if (canSeeNotificationsTab) {
@@ -208,6 +214,11 @@ function Organization({ i18n, setBreadcrumb, me }) {
/>
)}
+ {organization && (
+
+
+
+ )}
{!organizationLoading && !rolesLoading && (
diff --git a/awx/ui_next/src/screens/Organization/Organization.test.jsx b/awx/ui_next/src/screens/Organization/Organization.test.jsx
index 10982505d507..487ebff36b97 100644
--- a/awx/ui_next/src/screens/Organization/Organization.test.jsx
+++ b/awx/ui_next/src/screens/Organization/Organization.test.jsx
@@ -68,7 +68,7 @@ describe('', () => {
const tabs = await waitForElement(
wrapper,
'.pf-c-tabs__item',
- el => el.length === 5
+ el => el.length === 6
);
expect(tabs.last().text()).toEqual('Notifications');
wrapper.unmount();
@@ -92,7 +92,7 @@ describe('', () => {
const tabs = await waitForElement(
wrapper,
'.pf-c-tabs__item',
- el => el.length === 4
+ el => el.length === 5
);
tabs.forEach(tab => expect(tab.text()).not.toEqual('Notifications'));
wrapper.unmount();
diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx
new file mode 100644
index 000000000000..48caaff8028e
--- /dev/null
+++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx
@@ -0,0 +1,126 @@
+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 { OrganizationsAPI } from '../../../api';
+import { getQSConfig, parseQueryString } from '../../../util/qs';
+import useRequest from '../../../util/useRequest';
+import PaginatedDataList from '../../../components/PaginatedDataList';
+import DatalistToolbar from '../../../components/DataListToolbar';
+
+import OrganizationExecEnvListItem from './OrganizationExecEnvListItem';
+
+const QS_CONFIG = getQSConfig('organizations', {
+ page: 1,
+ page_size: 20,
+ order_by: 'image',
+});
+
+function OrganizationExecEnvList({ i18n, organization }) {
+ const { id } = organization;
+ const location = useLocation();
+
+ const {
+ error: contentError,
+ isLoading,
+ request: fetchExecutionEnvironments,
+ result: {
+ executionEnvironments,
+ executionEnvironmentsCount,
+ relatedSearchableKeys,
+ searchableKeys,
+ },
+ } = useRequest(
+ useCallback(async () => {
+ const params = parseQueryString(QS_CONFIG, location.search);
+
+ const [response, responseActions] = await Promise.all([
+ OrganizationsAPI.readExecutionEnvironments(id, params),
+ OrganizationsAPI.readExecutionEnvironmentsOptions(id, params),
+ ]);
+
+ return {
+ executionEnvironments: response.data.results,
+ executionEnvironmentsCount: 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]),
+ {
+ executionEnvironments: [],
+ executionEnvironmentsCount: 0,
+ actions: {},
+ relatedSearchableKeys: [],
+ searchableKeys: [],
+ }
+ );
+
+ useEffect(() => {
+ fetchExecutionEnvironments();
+ }, [fetchExecutionEnvironments]);
+
+ return (
+ <>
+
+ (
+
+ )}
+ renderItem={executionEnvironment => (
+
+ )}
+ />
+
+ >
+ );
+}
+
+export default withI18n()(OrganizationExecEnvList);
diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.test.jsx
new file mode 100644
index 000000000000..07e8a53ea5f6
--- /dev/null
+++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.test.jsx
@@ -0,0 +1,116 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+
+import {
+ mountWithContexts,
+ waitForElement,
+} from '../../../../testUtils/enzymeHelpers';
+
+import { OrganizationsAPI } from '../../../api';
+import OrganizationExecEnvList from './OrganizationExecEnvList';
+
+jest.mock('../../../api/');
+
+const executionEnvironments = {
+ data: {
+ count: 3,
+ results: [
+ {
+ id: 1,
+ type: 'execution_environment',
+ url: '/api/v2/execution_environments/1/',
+ related: {
+ organization: '/api/v2/organizations/1/',
+ },
+ organization: 1,
+ image: 'https://localhost.com/image/disk',
+ managed_by_tower: false,
+ credential: null,
+ },
+ {
+ id: 2,
+ type: 'execution_environment',
+ url: '/api/v2/execution_environments/2/',
+ related: {
+ organization: '/api/v2/organizations/1/',
+ },
+ organization: 1,
+ image: 'test/image123',
+ managed_by_tower: false,
+ credential: null,
+ },
+ {
+ id: 3,
+ type: 'execution_environment',
+ url: '/api/v2/execution_environments/3/',
+ related: {
+ organization: '/api/v2/organizations/1/',
+ },
+ organization: 1,
+ image: 'test/test',
+ managed_by_tower: false,
+ credential: null,
+ },
+ ],
+ },
+};
+
+const mockOrganization = {
+ id: 1,
+ type: 'organization',
+ name: 'Default',
+};
+
+const options = { data: { actions: { POST: {}, GET: {} } } };
+
+describe('', () => {
+ let wrapper;
+
+ test('should mount successfully', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ await waitForElement(
+ wrapper,
+ 'OrganizationExecEnvList',
+ el => el.length > 0
+ );
+ });
+
+ test('should have data fetched and render 3 rows', async () => {
+ OrganizationsAPI.readExecutionEnvironments.mockResolvedValue(
+ executionEnvironments
+ );
+
+ OrganizationsAPI.readExecutionEnvironmentsOptions.mockResolvedValue(
+ options
+ );
+
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ await waitForElement(
+ wrapper,
+ 'OrganizationExecEnvList',
+ el => el.length > 0
+ );
+
+ expect(wrapper.find('OrganizationExecEnvListItem').length).toBe(3);
+ expect(OrganizationsAPI.readExecutionEnvironments).toBeCalled();
+ expect(OrganizationsAPI.readExecutionEnvironmentsOptions).toBeCalled();
+ });
+
+ test('should not render add button', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ waitForElement(wrapper, 'OrganizationExecEnvList', el => el.length > 0);
+ expect(wrapper.find('ToolbarAddButton').length).toBe(0);
+ });
+});
diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.jsx b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.jsx
new file mode 100644
index 000000000000..1998e8c783a6
--- /dev/null
+++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.jsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { string } from 'prop-types';
+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';
+import { ExecutionEnvironment } from '../../../types';
+
+function OrganizationExecEnvListItem({
+ executionEnvironment,
+ detailUrl,
+ i18n,
+}) {
+ const labelId = `check-action-${executionEnvironment.id}`;
+
+ return (
+
+
+
+
+ {executionEnvironment.image}
+
+ ,
+ ]}
+ />
+
+
+ );
+}
+
+OrganizationExecEnvListItem.prototype = {
+ executionEnvironment: ExecutionEnvironment.isRequired,
+ detailUrl: string.isRequired,
+};
+
+export default withI18n()(OrganizationExecEnvListItem);
diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.test.jsx
new file mode 100644
index 000000000000..9e4a2492aaf0
--- /dev/null
+++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvListItem.test.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+
+import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
+
+import OrganizationExecEnvListItem from './OrganizationExecEnvListItem';
+
+describe('', () => {
+ let wrapper;
+ const executionEnvironment = {
+ id: 1,
+ image: 'https://registry.com/r/image/manifest',
+ organization: 1,
+ credential: null,
+ };
+
+ test('should mount successfully', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ expect(wrapper.find('OrganizationExecEnvListItem').length).toBe(1);
+ });
+
+ test('should render the proper data', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ expect(
+ wrapper
+ .find('DataListCell[aria-label="Execution environment image"]')
+ .text()
+ ).toBe(executionEnvironment.image);
+ });
+});
diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/index.js b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/index.js
new file mode 100644
index 000000000000..668a3beb619f
--- /dev/null
+++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/index.js
@@ -0,0 +1 @@
+export { default } from './OrganizationExecEnvList';
diff --git a/awx/ui_next/src/screens/Organization/Organizations.jsx b/awx/ui_next/src/screens/Organization/Organizations.jsx
index 6c7b17dc690e..fcf1b8398b6a 100644
--- a/awx/ui_next/src/screens/Organization/Organizations.jsx
+++ b/awx/ui_next/src/screens/Organization/Organizations.jsx
@@ -34,6 +34,9 @@ function Organizations({ i18n }) {
[`/organizations/${organization.id}/notifications`]: i18n._(
t`Notifications`
),
+ [`/organizations/${organization.id}/execution_environments`]: i18n._(
+ t`Execution Environments`
+ ),
};
setBreadcrumbConfig(breadcrumb);
},