From 04839a037aa7fdeb675572b4528e59025fd544e3 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Thu, 10 Jun 2021 10:06:08 -0400 Subject: [PATCH] prevent delete of instance groups list item controlplan and default --- awx/ui_next/src/api/models/Settings.js | 4 ++ .../screens/InstanceGroup/InstanceGroup.jsx | 32 ++++++++-- .../InstanceGroupEdit/InstanceGroupEdit.jsx | 8 ++- .../InstanceGroupEdit.test.jsx | 36 +++++++++-- .../InstanceGroupList/InstanceGroupList.jsx | 63 ++++++++++++------- .../InstanceGroupList.test.jsx | 27 ++++++-- .../shared/InstanceGroupForm.jsx | 9 ++- 7 files changed, 138 insertions(+), 41 deletions(-) diff --git a/awx/ui_next/src/api/models/Settings.js b/awx/ui_next/src/api/models/Settings.js index b5d0679e9c..440013037a 100644 --- a/awx/ui_next/src/api/models/Settings.js +++ b/awx/ui_next/src/api/models/Settings.js @@ -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); } diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx index 5d13650c3f..187109d8c6 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx @@ -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 && ( <> - + diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx index b2f9bbaa9a..166d8753f1 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx @@ -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 }) { ', () => { history = createMemoryHistory(); await act(async () => { wrapper = mountWithContexts( - , + , { context: { router: { history } }, } @@ -68,12 +72,14 @@ describe('', () => { 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( , { context: { router: { history } }, @@ -85,7 +91,29 @@ describe('', () => { ).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( + , + { + 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 () => { diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx index 2106ca9999..c6de8a9f3e 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx @@ -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({ ', () => { let wrapper; @@ -62,6 +78,7 @@ describe('', () => { 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('', () => { wrapper = mountWithContexts(); }); 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('', () => { ); }); - 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(); }); await waitForElement(wrapper, 'InstanceGroupList', el => el.length > 0); - const instanceGroupIndex = [0, 1, 2]; + const instanceGroupIndex = [0, 1, 2, 3]; instanceGroupIndex.forEach(element => { wrapper diff --git a/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.jsx b/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.jsx index 50f75e1831..a9a268af20 100644 --- a/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.jsx @@ -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 ( <>