mirror of
https://github.com/ansible/awx.git
synced 2026-01-24 16:01:20 -03:30
prevent delete of instance groups list item controlplan and default
This commit is contained in:
parent
f541fe9904
commit
04839a037a
@ -14,6 +14,10 @@ class Settings extends Base {
|
||||
return this.http.patch(`${this.baseUrl}all/`, data);
|
||||
}
|
||||
|
||||
readAll() {
|
||||
return this.http.get(`${this.baseUrl}all/`);
|
||||
}
|
||||
|
||||
updateCategory(category, data) {
|
||||
return this.http.patch(`${this.baseUrl}${category}/`, data);
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
|
||||
import useRequest from '../../util/useRequest';
|
||||
import { InstanceGroupsAPI } from '../../api';
|
||||
import { InstanceGroupsAPI, SettingsAPI } from '../../api';
|
||||
import RoutedTabs from '../../components/RoutedTabs';
|
||||
import ContentError from '../../components/ContentError';
|
||||
import ContentLoading from '../../components/ContentLoading';
|
||||
@ -31,12 +31,28 @@ function InstanceGroup({ setBreadcrumb }) {
|
||||
isLoading,
|
||||
error: contentError,
|
||||
request: fetchInstanceGroups,
|
||||
result: instanceGroup,
|
||||
result: { instanceGroup, defaultControlPlane, defaultExecution },
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const { data } = await InstanceGroupsAPI.readDetail(id);
|
||||
return data;
|
||||
}, [id])
|
||||
const [
|
||||
{ data },
|
||||
{
|
||||
data: {
|
||||
DEFAULT_CONTROL_PLANE_QUEUE_NAME,
|
||||
DEFAULT_EXECUTION_QUEUE_NAME,
|
||||
},
|
||||
},
|
||||
] = await Promise.all([
|
||||
InstanceGroupsAPI.readDetail(id),
|
||||
SettingsAPI.readAll(),
|
||||
]);
|
||||
return {
|
||||
instanceGroup: data,
|
||||
defaultControlPlane: DEFAULT_CONTROL_PLANE_QUEUE_NAME,
|
||||
defaultExecution: DEFAULT_EXECUTION_QUEUE_NAME,
|
||||
};
|
||||
}, [id]),
|
||||
{ instanceGroup: {}, defaultControlPlane: '', defaultExecution: '' }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -115,7 +131,11 @@ function InstanceGroup({ setBreadcrumb }) {
|
||||
{instanceGroup && (
|
||||
<>
|
||||
<Route path="/instance_groups/:id/edit">
|
||||
<InstanceGroupEdit instanceGroup={instanceGroup} />
|
||||
<InstanceGroupEdit
|
||||
instanceGroup={instanceGroup}
|
||||
defaultExecution={defaultExecution}
|
||||
defaultControlPlane={defaultControlPlane}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/instance_groups/:id/details">
|
||||
<InstanceGroupDetails instanceGroup={instanceGroup} />
|
||||
|
||||
@ -5,7 +5,11 @@ import { CardBody } from '../../../components/Card';
|
||||
import { InstanceGroupsAPI } from '../../../api';
|
||||
import InstanceGroupForm from '../shared/InstanceGroupForm';
|
||||
|
||||
function InstanceGroupEdit({ instanceGroup }) {
|
||||
function InstanceGroupEdit({
|
||||
instanceGroup,
|
||||
defaultExecution,
|
||||
defaultControlPlane,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const [submitError, setSubmitError] = useState(null);
|
||||
const detailsUrl = `/instance_groups/${instanceGroup.id}/details`;
|
||||
@ -27,6 +31,8 @@ function InstanceGroupEdit({ instanceGroup }) {
|
||||
<CardBody>
|
||||
<InstanceGroupForm
|
||||
instanceGroup={instanceGroup}
|
||||
defaultExecution={defaultExecution}
|
||||
defaultControlPlane={defaultControlPlane}
|
||||
onSubmit={handleSubmit}
|
||||
submitError={submitError}
|
||||
onCancel={handleCancel}
|
||||
|
||||
@ -55,7 +55,11 @@ describe('<InstanceGroupEdit>', () => {
|
||||
history = createMemoryHistory();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InstanceGroupEdit instanceGroup={instanceGroupData} />,
|
||||
<InstanceGroupEdit
|
||||
defaultExecution="default"
|
||||
defaultControlPlane="controlplane"
|
||||
instanceGroup={instanceGroupData}
|
||||
/>,
|
||||
{
|
||||
context: { router: { history } },
|
||||
}
|
||||
@ -68,12 +72,14 @@ describe('<InstanceGroupEdit>', () => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('tower instance group name can not be updated', async () => {
|
||||
test('controlplane instance group name can not be updated', async () => {
|
||||
let towerWrapper;
|
||||
await act(async () => {
|
||||
towerWrapper = mountWithContexts(
|
||||
<InstanceGroupEdit
|
||||
instanceGroup={{ ...instanceGroupData, name: 'tower' }}
|
||||
defaultExecution="default"
|
||||
defaultControlPlane="controlplane"
|
||||
instanceGroup={{ ...instanceGroupData, name: 'controlplane' }}
|
||||
/>,
|
||||
{
|
||||
context: { router: { history } },
|
||||
@ -85,7 +91,29 @@ describe('<InstanceGroupEdit>', () => {
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
towerWrapper.find('input#instance-group-name').prop('value')
|
||||
).toEqual('tower');
|
||||
).toEqual('controlplane');
|
||||
});
|
||||
|
||||
test('default instance group name can not be updated', async () => {
|
||||
let towerWrapper;
|
||||
await act(async () => {
|
||||
towerWrapper = mountWithContexts(
|
||||
<InstanceGroupEdit
|
||||
defaultExecution="default"
|
||||
defaultControlPlane="controlplane"
|
||||
instanceGroup={{ ...instanceGroupData, name: 'default' }}
|
||||
/>,
|
||||
{
|
||||
context: { router: { history } },
|
||||
}
|
||||
);
|
||||
});
|
||||
expect(
|
||||
towerWrapper.find('input#instance-group-name').prop('disabled')
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
towerWrapper.find('input#instance-group-name').prop('value')
|
||||
).toEqual('default');
|
||||
});
|
||||
|
||||
test('handleSubmit should call the api and redirect to details page', async () => {
|
||||
|
||||
@ -4,7 +4,7 @@ import { useLocation, useRouteMatch, Link } from 'react-router-dom';
|
||||
import { t, Plural } from '@lingui/macro';
|
||||
import { Card, PageSection, DropdownItem } from '@patternfly/react-core';
|
||||
|
||||
import { InstanceGroupsAPI } from '../../../api';
|
||||
import { InstanceGroupsAPI, SettingsAPI } from '../../../api';
|
||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
||||
import useSelected from '../../../util/useSelected';
|
||||
@ -26,7 +26,11 @@ const QS_CONFIG = getQSConfig('instance-group', {
|
||||
page_size: 20,
|
||||
});
|
||||
|
||||
function modifyInstanceGroups(items = []) {
|
||||
function modifyInstanceGroups(
|
||||
items = [],
|
||||
defaultControlPlane,
|
||||
defaultExecution
|
||||
) {
|
||||
return items.map(item => {
|
||||
const clonedItem = {
|
||||
...item,
|
||||
@ -37,7 +41,7 @@ function modifyInstanceGroups(items = []) {
|
||||
},
|
||||
},
|
||||
};
|
||||
if (clonedItem.name === 'tower') {
|
||||
if (clonedItem.name === (defaultControlPlane || defaultExecution)) {
|
||||
clonedItem.summary_fields.user_capabilities.delete = false;
|
||||
}
|
||||
return clonedItem;
|
||||
@ -62,18 +66,32 @@ function InstanceGroupList({
|
||||
actions,
|
||||
relatedSearchableKeys,
|
||||
searchableKeys,
|
||||
defaultControlPlane,
|
||||
defaultExecution,
|
||||
},
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
|
||||
const [response, responseActions] = await Promise.all([
|
||||
const [
|
||||
response,
|
||||
responseActions,
|
||||
{
|
||||
data: {
|
||||
DEFAULT_CONTROL_PLANE_QUEUE_NAME,
|
||||
DEFAULT_EXECUTION_QUEUE_NAME,
|
||||
},
|
||||
},
|
||||
] = await Promise.all([
|
||||
InstanceGroupsAPI.read(params),
|
||||
InstanceGroupsAPI.readOptions(),
|
||||
SettingsAPI.readAll(),
|
||||
]);
|
||||
|
||||
return {
|
||||
instanceGroups: response.data.results,
|
||||
defaultControlPlane: DEFAULT_CONTROL_PLANE_QUEUE_NAME,
|
||||
defaultExecution: DEFAULT_EXECUTION_QUEUE_NAME,
|
||||
instanceGroupsCount: response.data.count,
|
||||
actions: responseActions.data.actions,
|
||||
relatedSearchableKeys: (
|
||||
@ -105,7 +123,11 @@ function InstanceGroupList({
|
||||
selectAll,
|
||||
} = useSelected(instanceGroups);
|
||||
|
||||
const modifiedSelected = modifyInstanceGroups(selected);
|
||||
const modifiedSelected = modifyInstanceGroups(
|
||||
selected,
|
||||
defaultControlPlane,
|
||||
defaultExecution
|
||||
);
|
||||
|
||||
const {
|
||||
isLoading: deleteLoading,
|
||||
@ -133,31 +155,25 @@ function InstanceGroupList({
|
||||
const canAdd = actions && actions.POST;
|
||||
|
||||
function cannotDelete(item) {
|
||||
return !item.summary_fields.user_capabilities.delete;
|
||||
return (
|
||||
!item.summary_fields.user_capabilities.delete ||
|
||||
item.name === defaultExecution ||
|
||||
item.name === defaultControlPlane
|
||||
);
|
||||
}
|
||||
|
||||
const pluralizedItemName = t`Instance Groups`;
|
||||
|
||||
let errorMessageDelete = '';
|
||||
|
||||
if (modifiedSelected.some(item => item.name === 'tower')) {
|
||||
const itemsUnableToDelete = modifiedSelected
|
||||
.filter(cannotDelete)
|
||||
.filter(item => item.name !== 'tower')
|
||||
.map(item => item.name)
|
||||
.join(', ');
|
||||
|
||||
if (itemsUnableToDelete) {
|
||||
if (modifiedSelected.some(cannotDelete)) {
|
||||
errorMessageDelete = t`You do not have permission to delete ${pluralizedItemName}: ${itemsUnableToDelete}. `;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessageDelete.length > 0) {
|
||||
errorMessageDelete = errorMessageDelete.concat('\n');
|
||||
}
|
||||
if (
|
||||
modifiedSelected.some(
|
||||
item =>
|
||||
item.name === defaultControlPlane || item.name === defaultExecution
|
||||
)
|
||||
) {
|
||||
errorMessageDelete = errorMessageDelete.concat(
|
||||
t`The tower instance group cannot be deleted.`
|
||||
t`The following Instance Group cannot be deleted`
|
||||
);
|
||||
}
|
||||
|
||||
@ -234,6 +250,7 @@ function InstanceGroupList({
|
||||
<ToolbarDeleteButton
|
||||
key="delete"
|
||||
onDelete={handleDelete}
|
||||
cannotDelete={cannotDelete}
|
||||
itemsToDelete={modifiedSelected}
|
||||
pluralizedItemName={t`Instance Groups`}
|
||||
errorMessage={errorMessageDelete}
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
OrganizationsAPI,
|
||||
InventoriesAPI,
|
||||
UnifiedJobTemplatesAPI,
|
||||
SettingsAPI,
|
||||
} from '../../../api';
|
||||
import InstanceGroupList from './InstanceGroupList';
|
||||
|
||||
@ -18,6 +19,7 @@ jest.mock('../../../api/models/InstanceGroups');
|
||||
jest.mock('../../../api/models/Organizations');
|
||||
jest.mock('../../../api/models/Inventories');
|
||||
jest.mock('../../../api/models/UnifiedJobTemplates');
|
||||
jest.mock('../../../api/models/Settings');
|
||||
|
||||
const instanceGroups = {
|
||||
data: {
|
||||
@ -32,7 +34,7 @@ const instanceGroups = {
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'tower',
|
||||
name: 'controlplan',
|
||||
type: 'instance_group',
|
||||
url: '/api/v2/instance_groups/2',
|
||||
consumed_capacity: 42,
|
||||
@ -40,6 +42,14 @@ const instanceGroups = {
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'default',
|
||||
type: 'instance_group',
|
||||
url: '/api/v2/instance_groups/2',
|
||||
consumed_capacity: 42,
|
||||
summary_fields: { user_capabilities: { edit: true, delete: true } },
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Bar',
|
||||
type: 'instance_group',
|
||||
url: '/api/v2/instance_groups/3',
|
||||
@ -47,11 +57,17 @@ const instanceGroups = {
|
||||
summary_fields: { user_capabilities: { edit: true, delete: false } },
|
||||
},
|
||||
],
|
||||
count: 3,
|
||||
count: 4,
|
||||
},
|
||||
};
|
||||
|
||||
const options = { data: { actions: { POST: true } } };
|
||||
const settings = {
|
||||
data: {
|
||||
DEFAULT_CONTROL_PLANE_QUEUE_NAME: 'controlplan',
|
||||
DEFAULT_EXECUTION_QUEUE_NAME: 'default',
|
||||
},
|
||||
};
|
||||
|
||||
describe('<InstanceGroupList />', () => {
|
||||
let wrapper;
|
||||
@ -62,6 +78,7 @@ describe('<InstanceGroupList />', () => {
|
||||
UnifiedJobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||
InstanceGroupsAPI.read.mockResolvedValue(instanceGroups);
|
||||
InstanceGroupsAPI.readOptions.mockResolvedValue(options);
|
||||
SettingsAPI.readAll.mockResolvedValue(settings);
|
||||
});
|
||||
|
||||
test('should have data fetched and render 3 rows', async () => {
|
||||
@ -69,7 +86,7 @@ describe('<InstanceGroupList />', () => {
|
||||
wrapper = mountWithContexts(<InstanceGroupList />);
|
||||
});
|
||||
await waitForElement(wrapper, 'InstanceGroupList', el => el.length > 0);
|
||||
expect(wrapper.find('InstanceGroupListItem').length).toBe(3);
|
||||
expect(wrapper.find('InstanceGroupListItem').length).toBe(4);
|
||||
expect(InstanceGroupsAPI.read).toBeCalled();
|
||||
expect(InstanceGroupsAPI.readOptions).toBeCalled();
|
||||
});
|
||||
@ -109,13 +126,13 @@ describe('<InstanceGroupList />', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should not be able to delete tower instance group', async () => {
|
||||
test('should not be able to delete controlplan or default instance group', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InstanceGroupList />);
|
||||
});
|
||||
await waitForElement(wrapper, 'InstanceGroupList', el => el.length > 0);
|
||||
|
||||
const instanceGroupIndex = [0, 1, 2];
|
||||
const instanceGroupIndex = [0, 1, 2, 3];
|
||||
|
||||
instanceGroupIndex.forEach(element => {
|
||||
wrapper
|
||||
|
||||
@ -10,8 +10,9 @@ import FormActionGroup from '../../../components/FormActionGroup';
|
||||
import { required, minMaxValue } from '../../../util/validators';
|
||||
import { FormColumnLayout } from '../../../components/FormLayout';
|
||||
|
||||
function InstanceGroupFormFields() {
|
||||
function InstanceGroupFormFields({ defaultExecution, defaultControlPlane }) {
|
||||
const [instanceGroupNameField, ,] = useField('name');
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
@ -21,7 +22,10 @@ function InstanceGroupFormFields() {
|
||||
type="text"
|
||||
validate={required(null)}
|
||||
isRequired
|
||||
isDisabled={instanceGroupNameField.value === 'tower'}
|
||||
isDisabled={
|
||||
instanceGroupNameField.value === defaultExecution ||
|
||||
instanceGroupNameField.value === defaultControlPlane
|
||||
}
|
||||
/>
|
||||
<FormField
|
||||
id="instance-group-policy-instance-minimum"
|
||||
@ -50,6 +54,7 @@ function InstanceGroupFormFields() {
|
||||
|
||||
function InstanceGroupForm({
|
||||
instanceGroup = {},
|
||||
|
||||
onSubmit,
|
||||
onCancel,
|
||||
submitError,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user