diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx
index 68b9f9879d..e777003c92 100644
--- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx
@@ -26,6 +26,7 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) {
pull,
organization,
summary_fields,
+ managed_by_tower: managedByTower,
} = executionEnvironment;
const {
@@ -103,25 +104,27 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) {
dataCy="execution-environment-modified"
/>
-
-
-
- {i18n._(t`Delete`)}
-
-
+ {!managedByTower && (
+
+
+
+ {i18n._(t`Delete`)}
+
+
+ )}
{error && (
', () => {
expect(dates).toHaveLength(2);
expect(dates.at(0).prop('date')).toEqual(executionEnvironment.created);
expect(dates.at(1).prop('date')).toEqual(executionEnvironment.modified);
+ const editButton = wrapper.find('Button[aria-label="edit"]');
+ expect(editButton.text()).toEqual('Edit');
+ expect(editButton.prop('to')).toBe('/execution_environments/17/edit');
+
+ const deleteButton = wrapper.find('Button[aria-label="Delete"]');
+ expect(deleteButton.text()).toEqual('Delete');
});
test('should render organization detail', async () => {
@@ -135,4 +141,38 @@ describe('', () => {
expect(ExecutionEnvironmentsAPI.destroy).toHaveBeenCalledTimes(1);
expect(history.location.pathname).toBe('/execution_environments');
});
+
+ test('should not render action buttons to ee managed by tower', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
+
+ expect(wrapper.find('Detail[label="Image"]').prop('value')).toEqual(
+ executionEnvironment.image
+ );
+ expect(wrapper.find('Detail[label="Description"]').prop('value')).toEqual(
+ 'Foo'
+ );
+ expect(wrapper.find('Detail[label="Organization"]').prop('value')).toEqual(
+ 'Globally Available'
+ );
+ expect(
+ wrapper.find('Detail[label="Credential"]').prop('value').props.children
+ ).toEqual(executionEnvironment.summary_fields.credential.name);
+ const dates = wrapper.find('UserDateDetail');
+ expect(dates).toHaveLength(2);
+ expect(dates.at(0).prop('date')).toEqual(executionEnvironment.created);
+ expect(dates.at(1).prop('date')).toEqual(executionEnvironment.modified);
+ expect(wrapper.find('Button[aria-label="edit"]')).toHaveLength(0);
+
+ expect(wrapper.find('Button[aria-label="Delete"]')).toHaveLength(0);
+ });
});
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx
index 278566e632..914b884f1f 100644
--- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx
@@ -195,6 +195,7 @@ function ExecutionEnvironmentList({ i18n }) {
isSelected={selected.some(
row => row.id === executionEnvironment.id
)}
+ fetchExecutionEnvironments={fetchExecutionEnvironments}
/>
)}
emptyStateControls={
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx
index 1f56b53727..07816ff5fc 100644
--- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.jsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState, useCallback } from 'react';
import { string, bool, func } from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
@@ -8,7 +8,10 @@ import { Tr, Td } from '@patternfly/react-table';
import { PencilAltIcon } from '@patternfly/react-icons';
import { ActionsTd, ActionItem } from '../../../components/PaginatedTable';
+import CopyButton from '../../../components/CopyButton';
import { ExecutionEnvironment } from '../../../types';
+import { ExecutionEnvironmentsAPI } from '../../../api';
+import { timeOfDay } from '../../../util/dates';
function ExecutionEnvironmentListItem({
executionEnvironment,
@@ -17,7 +20,29 @@ function ExecutionEnvironmentListItem({
onSelect,
i18n,
rowIndex,
+ fetchExecutionEnvironments,
}) {
+ const [isDisabled, setIsDisabled] = useState(false);
+
+ const copyExecutionEnvironment = useCallback(async () => {
+ await ExecutionEnvironmentsAPI.copy(executionEnvironment.id, {
+ name: `${executionEnvironment.name} @ ${timeOfDay()}`,
+ });
+ await fetchExecutionEnvironments();
+ }, [
+ executionEnvironment.id,
+ executionEnvironment.name,
+ fetchExecutionEnvironments,
+ ]);
+
+ const handleCopyStart = useCallback(() => {
+ setIsDisabled(true);
+ }, []);
+
+ const handleCopyFinish = useCallback(() => {
+ setIsDisabled(false);
+ }, []);
+
const labelId = `check-action-${executionEnvironment.id}`;
return (
@@ -65,6 +90,19 @@ function ExecutionEnvironmentListItem({
+
+
+
);
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.test.jsx
index 0e7c037aed..0df9bfcf31 100644
--- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.test.jsx
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentListItem.test.jsx
@@ -4,8 +4,15 @@ import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import ExecutionEnvironmentListItem from './ExecutionEnvironmentListItem';
+import { ExecutionEnvironmentsAPI } from '../../../api';
+
+jest.mock('../../../api');
describe('', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
let wrapper;
const executionEnvironment = {
name: 'Foo',
@@ -13,7 +20,10 @@ describe('', () => {
image: 'https://registry.com/r/image/manifest',
organization: null,
credential: null,
- summary_fields: { user_capabilities: { edit: true } },
+ summary_fields: {
+ user_capabilities: { edit: true, copy: true, delete: true },
+ },
+ managed_by_tower: false,
};
test('should mount successfully', async () => {
@@ -71,4 +81,110 @@ describe('', () => {
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
});
+
+ test('should call api to copy execution environment', async () => {
+ ExecutionEnvironmentsAPI.copy.mockResolvedValue();
+
+ wrapper = mountWithContexts(
+
+ );
+
+ await act(async () =>
+ wrapper.find('Button[aria-label="Copy"]').prop('onClick')()
+ );
+ expect(ExecutionEnvironmentsAPI.copy).toHaveBeenCalled();
+ });
+
+ test('should render proper alert modal on copy error', async () => {
+ ExecutionEnvironmentsAPI.copy.mockRejectedValue(new Error());
+
+ wrapper = mountWithContexts(
+
+ );
+
+ await act(async () =>
+ wrapper.find('Button[aria-label="Copy"]').prop('onClick')()
+ );
+ wrapper.update();
+ expect(wrapper.find('Modal').prop('isOpen')).toBe(true);
+ });
+
+ test('should not render copy button', async () => {
+ wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.find('CopyButton').length).toBe(0);
+ });
+
+ test('should not render the pencil action for ee managed by tower', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ expect(
+ wrapper
+ .find('Td')
+ .at(1)
+ .text()
+ ).toBe(executionEnvironment.name);
+ expect(
+ wrapper
+ .find('Td')
+ .at(2)
+ .text()
+ ).toBe(executionEnvironment.image);
+
+ expect(
+ wrapper
+ .find('Td')
+ .at(3)
+ .text()
+ ).toBe('Globally Available');
+
+ expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
+ });
});