prevent delete of instance groups list item controlplan and default

This commit is contained in:
Alex Corey 2021-06-10 10:06:08 -04:00 committed by Shane McDonald
parent f541fe9904
commit 04839a037a
No known key found for this signature in database
GPG Key ID: 6F374AF6E9EB9374
7 changed files with 138 additions and 41 deletions

View File

@ -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);
}

View File

@ -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} />

View File

@ -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}

View File

@ -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 () => {

View File

@ -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}

View File

@ -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

View File

@ -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,