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