mirror of
https://github.com/ansible/awx.git
synced 2026-02-03 10:38:15 -03:30
Merge pull request #9595 from nixocio/ui_issue_9307
Add copy functionality to EE Add copy functionality to EE. See: #9307 Reviewed-by: Jake McDermott <yo@jakemcdermott.me> Reviewed-by: Tiago Góes <tiago.goes2009@gmail.com>
This commit is contained in:
@@ -26,6 +26,7 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) {
|
|||||||
pull,
|
pull,
|
||||||
organization,
|
organization,
|
||||||
summary_fields,
|
summary_fields,
|
||||||
|
managed_by_tower: managedByTower,
|
||||||
} = executionEnvironment;
|
} = executionEnvironment;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -103,25 +104,27 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) {
|
|||||||
dataCy="execution-environment-modified"
|
dataCy="execution-environment-modified"
|
||||||
/>
|
/>
|
||||||
</DetailList>
|
</DetailList>
|
||||||
<CardActionsRow>
|
{!managedByTower && (
|
||||||
<Button
|
<CardActionsRow>
|
||||||
aria-label={i18n._(t`edit`)}
|
<Button
|
||||||
component={Link}
|
aria-label={i18n._(t`edit`)}
|
||||||
to={`/execution_environments/${id}/edit`}
|
component={Link}
|
||||||
ouiaId="edit-button"
|
to={`/execution_environments/${id}/edit`}
|
||||||
>
|
ouiaId="edit-button"
|
||||||
{i18n._(t`Edit`)}
|
>
|
||||||
</Button>
|
{i18n._(t`Edit`)}
|
||||||
<DeleteButton
|
</Button>
|
||||||
name={image}
|
<DeleteButton
|
||||||
modalTitle={i18n._(t`Delete Execution Environment`)}
|
name={image}
|
||||||
onConfirm={deleteExecutionEnvironment}
|
modalTitle={i18n._(t`Delete Execution Environment`)}
|
||||||
isDisabled={isLoading}
|
onConfirm={deleteExecutionEnvironment}
|
||||||
ouiaId="delete-button"
|
isDisabled={isLoading}
|
||||||
>
|
ouiaId="delete-button"
|
||||||
{i18n._(t`Delete`)}
|
>
|
||||||
</DeleteButton>
|
{i18n._(t`Delete`)}
|
||||||
</CardActionsRow>
|
</DeleteButton>
|
||||||
|
</CardActionsRow>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ describe('<ExecutionEnvironmentDetails/>', () => {
|
|||||||
expect(dates).toHaveLength(2);
|
expect(dates).toHaveLength(2);
|
||||||
expect(dates.at(0).prop('date')).toEqual(executionEnvironment.created);
|
expect(dates.at(0).prop('date')).toEqual(executionEnvironment.created);
|
||||||
expect(dates.at(1).prop('date')).toEqual(executionEnvironment.modified);
|
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 () => {
|
test('should render organization detail', async () => {
|
||||||
@@ -135,4 +141,38 @@ describe('<ExecutionEnvironmentDetails/>', () => {
|
|||||||
expect(ExecutionEnvironmentsAPI.destroy).toHaveBeenCalledTimes(1);
|
expect(ExecutionEnvironmentsAPI.destroy).toHaveBeenCalledTimes(1);
|
||||||
expect(history.location.pathname).toBe('/execution_environments');
|
expect(history.location.pathname).toBe('/execution_environments');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should not render action buttons to ee managed by tower', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<ExecutionEnvironmentDetails
|
||||||
|
executionEnvironment={{
|
||||||
|
...executionEnvironment,
|
||||||
|
managed_by_tower: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ function ExecutionEnvironmentList({ i18n }) {
|
|||||||
isSelected={selected.some(
|
isSelected={selected.some(
|
||||||
row => row.id === executionEnvironment.id
|
row => row.id === executionEnvironment.id
|
||||||
)}
|
)}
|
||||||
|
fetchExecutionEnvironments={fetchExecutionEnvironments}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
emptyStateControls={
|
emptyStateControls={
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { string, bool, func } from 'prop-types';
|
import { string, bool, func } from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -8,7 +8,10 @@ import { Tr, Td } from '@patternfly/react-table';
|
|||||||
import { PencilAltIcon } from '@patternfly/react-icons';
|
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
import { ActionsTd, ActionItem } from '../../../components/PaginatedTable';
|
import { ActionsTd, ActionItem } from '../../../components/PaginatedTable';
|
||||||
|
import CopyButton from '../../../components/CopyButton';
|
||||||
import { ExecutionEnvironment } from '../../../types';
|
import { ExecutionEnvironment } from '../../../types';
|
||||||
|
import { ExecutionEnvironmentsAPI } from '../../../api';
|
||||||
|
import { timeOfDay } from '../../../util/dates';
|
||||||
|
|
||||||
function ExecutionEnvironmentListItem({
|
function ExecutionEnvironmentListItem({
|
||||||
executionEnvironment,
|
executionEnvironment,
|
||||||
@@ -17,7 +20,29 @@ function ExecutionEnvironmentListItem({
|
|||||||
onSelect,
|
onSelect,
|
||||||
i18n,
|
i18n,
|
||||||
rowIndex,
|
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}`;
|
const labelId = `check-action-${executionEnvironment.id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -65,6 +90,19 @@ function ExecutionEnvironmentListItem({
|
|||||||
<PencilAltIcon />
|
<PencilAltIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</ActionItem>
|
</ActionItem>
|
||||||
|
<ActionItem
|
||||||
|
visible={executionEnvironment.summary_fields.user_capabilities.copy}
|
||||||
|
tooltip={i18n._(t`Copy Execution Environment`)}
|
||||||
|
>
|
||||||
|
<CopyButton
|
||||||
|
ouiaId={`copy-ee-${executionEnvironment.id}`}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onCopyStart={handleCopyStart}
|
||||||
|
onCopyFinish={handleCopyFinish}
|
||||||
|
copyItem={copyExecutionEnvironment}
|
||||||
|
errorMessage={i18n._(t`Failed to copy execution environment`)}
|
||||||
|
/>
|
||||||
|
</ActionItem>
|
||||||
</ActionsTd>
|
</ActionsTd>
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,8 +4,15 @@ import { act } from 'react-dom/test-utils';
|
|||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
import ExecutionEnvironmentListItem from './ExecutionEnvironmentListItem';
|
import ExecutionEnvironmentListItem from './ExecutionEnvironmentListItem';
|
||||||
|
import { ExecutionEnvironmentsAPI } from '../../../api';
|
||||||
|
|
||||||
|
jest.mock('../../../api');
|
||||||
|
|
||||||
describe('<ExecutionEnvironmentListItem/>', () => {
|
describe('<ExecutionEnvironmentListItem/>', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
const executionEnvironment = {
|
const executionEnvironment = {
|
||||||
name: 'Foo',
|
name: 'Foo',
|
||||||
@@ -13,7 +20,10 @@ describe('<ExecutionEnvironmentListItem/>', () => {
|
|||||||
image: 'https://registry.com/r/image/manifest',
|
image: 'https://registry.com/r/image/manifest',
|
||||||
organization: null,
|
organization: null,
|
||||||
credential: 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 () => {
|
test('should mount successfully', async () => {
|
||||||
@@ -71,4 +81,110 @@ describe('<ExecutionEnvironmentListItem/>', () => {
|
|||||||
|
|
||||||
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
|
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should call api to copy execution environment', async () => {
|
||||||
|
ExecutionEnvironmentsAPI.copy.mockResolvedValue();
|
||||||
|
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<ExecutionEnvironmentListItem
|
||||||
|
executionEnvironment={executionEnvironment}
|
||||||
|
detailUrl="execution_environments/1/details"
|
||||||
|
isSelected={false}
|
||||||
|
onSelect={() => {}}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<ExecutionEnvironmentListItem
|
||||||
|
executionEnvironment={executionEnvironment}
|
||||||
|
detailUrl="execution_environments/1/details"
|
||||||
|
isSelected={false}
|
||||||
|
onSelect={() => {}}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<ExecutionEnvironmentListItem
|
||||||
|
executionEnvironment={{
|
||||||
|
...executionEnvironment,
|
||||||
|
summary_fields: { user_capabilities: { copy: false } },
|
||||||
|
}}
|
||||||
|
detailUrl="execution_environments/1/details"
|
||||||
|
isSelected={false}
|
||||||
|
onSelect={() => {}}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<ExecutionEnvironmentListItem
|
||||||
|
executionEnvironment={{
|
||||||
|
...executionEnvironment,
|
||||||
|
summary_fields: { user_capabilities: { edit: false } },
|
||||||
|
managed_by_tower: true,
|
||||||
|
}}
|
||||||
|
detailUrl="execution_environments/1/details"
|
||||||
|
isSelected={false}
|
||||||
|
onSelect={() => {}}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user