Add several changes to Instance Groups

Add several changes to API and UI related to Instance Groups.

* Update summary_fields for DEFAULT_CONTROL_PLANE_QUEUE_NAME, and
  DEFAULT_EXECUTION_QUEUE_NAME. Rely on API validation for those fields.

* Fix Instance Group list RBAC

* Add validation for a couple of fields on the Instance Groups endpoint
	1. is_container_group
	2. policy_instance_percentage
	3. policy_instance_list

See: https://github.com/ansible/awx/issues/11130
Also: https://github.com/ansible/awx/issues/11718
This commit is contained in:
nixocio 2022-02-25 12:30:22 -05:00
parent 2e4d866f69
commit ce8b9750c9
20 changed files with 80 additions and 314 deletions

View File

@ -4,8 +4,6 @@
# Python
import logging
from django.conf import settings
# Django REST Framework
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
from rest_framework import permissions
@ -250,13 +248,6 @@ class IsSystemAdminOrAuditor(permissions.BasePermission):
return request.user.is_superuser
class InstanceGroupTowerPermission(ModelAccessPermission):
def has_object_permission(self, request, view, obj):
if request.method == 'DELETE' and obj.name in [settings.DEFAULT_EXECUTION_QUEUE_NAME, settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME]:
return False
return super(InstanceGroupTowerPermission, self).has_object_permission(request, view, obj)
class WebhookKeyPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return request.user.can_access(view.model, 'admin', obj, request.data)

View File

@ -4947,6 +4947,9 @@ class InstanceGroupSerializer(BaseSerializer):
return res
def validate_policy_instance_list(self, value):
if self.instance and self.instance.name in [settings.DEFAULT_EXECUTION_QUEUE_NAME, settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME]:
if self.instance.policy_instance_list != value:
raise serializers.ValidationError(_('%s instance group policy_instance_list may not be changed.' % self.instance.name))
for instance_name in value:
if value.count(instance_name) > 1:
raise serializers.ValidationError(_('Duplicate entry {}.').format(instance_name))
@ -4957,6 +4960,11 @@ class InstanceGroupSerializer(BaseSerializer):
return value
def validate_policy_instance_percentage(self, value):
if self.instance and self.instance.name in [settings.DEFAULT_EXECUTION_QUEUE_NAME, settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME]:
if value != self.instance.policy_instance_percentage:
raise serializers.ValidationError(
_('%s instance group policy_instance_percentage may not be changed from the initial value set by the installer.' % self.instance.name)
)
if value and self.instance and self.instance.is_container_group:
raise serializers.ValidationError(_('Containerized instances may not be managed via the API'))
return value
@ -4975,6 +4983,13 @@ class InstanceGroupSerializer(BaseSerializer):
return value
def validate_is_container_group(self, value):
if self.instance and self.instance.name in [settings.DEFAULT_EXECUTION_QUEUE_NAME, settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME]:
if value != self.instance.is_container_group:
raise serializers.ValidationError(_('%s instance group is_container_group may not be changed.' % self.instance.name))
return value
def validate_credential(self, value):
if value and not value.kubernetes:
raise serializers.ValidationError(_('Only Kubernetes credentials can be associated with an Instance Group'))

View File

@ -105,7 +105,6 @@ from awx.api.permissions import (
ProjectUpdatePermission,
InventoryInventorySourcesUpdatePermission,
UserPermission,
InstanceGroupTowerPermission,
VariableDataPermission,
WorkflowApprovalPermission,
IsSystemAdminOrAuditor,
@ -480,7 +479,6 @@ class InstanceGroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAP
name = _("Instance Group Detail")
model = models.InstanceGroup
serializer_class = serializers.InstanceGroupSerializer
permission_classes = (InstanceGroupTowerPermission,)
def update_raw_data(self, data):
if self.get_object().is_container_group:

View File

@ -465,7 +465,7 @@ class BaseAccess(object):
if display_method == 'schedule':
user_capabilities['schedule'] = user_capabilities['start']
continue
elif display_method == 'delete' and not isinstance(obj, (User, UnifiedJob, CredentialInputSource, ExecutionEnvironment)):
elif display_method == 'delete' and not isinstance(obj, (User, UnifiedJob, CredentialInputSource, ExecutionEnvironment, InstanceGroup)):
user_capabilities['delete'] = user_capabilities['edit']
continue
elif display_method == 'copy' and isinstance(obj, (Group, Host)):
@ -575,6 +575,11 @@ class InstanceGroupAccess(BaseAccess):
def can_change(self, obj, data):
return self.user.is_superuser
def can_delete(self, obj):
if obj.name in [settings.DEFAULT_EXECUTION_QUEUE_NAME, settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME]:
return False
return self.user.is_superuser
class UserAccess(BaseAccess):
"""

View File

@ -13,7 +13,7 @@ import { CaretLeftIcon } from '@patternfly/react-icons';
import { Card, PageSection } from '@patternfly/react-core';
import useRequest from 'hooks/useRequest';
import { InstanceGroupsAPI, SettingsAPI } from 'api';
import { InstanceGroupsAPI } from 'api';
import RoutedTabs from 'components/RoutedTabs';
import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading';
@ -30,28 +30,15 @@ function ContainerGroup({ setBreadcrumb }) {
isLoading,
error: contentError,
request: fetchInstanceGroups,
result: { instanceGroup, defaultControlPlane, defaultExecution },
result: { instanceGroup },
} = useRequest(
useCallback(async () => {
const [
{ data },
{
data: {
DEFAULT_EXECUTION_QUEUE_NAME,
DEFAULT_CONTROL_PLANE_QUEUE_NAME,
},
},
] = await Promise.all([
InstanceGroupsAPI.readDetail(id),
SettingsAPI.readAll(),
]);
const { data } = await InstanceGroupsAPI.readDetail(id);
return {
instanceGroup: data,
defaultControlPlane: DEFAULT_CONTROL_PLANE_QUEUE_NAME,
defaultExecution: DEFAULT_EXECUTION_QUEUE_NAME,
};
}, [id]),
{ instanceGroup: null, defaultExecution: '' }
{ instanceGroup: null }
);
useEffect(() => {
@ -125,17 +112,10 @@ function ContainerGroup({ setBreadcrumb }) {
{instanceGroup && (
<>
<Route path="/instance_groups/container_group/:id/edit">
<ContainerGroupEdit
instanceGroup={instanceGroup}
defaultControlPlane={defaultControlPlane}
defaultExecution={defaultExecution}
/>
<ContainerGroupEdit instanceGroup={instanceGroup} />
</Route>
<Route path="/instance_groups/container_group/:id/details">
<ContainerGroupDetails
instanceGroup={instanceGroup}
defaultExecution={defaultExecution}
/>
<ContainerGroupDetails instanceGroup={instanceGroup} />
</Route>
<Route path="/instance_groups/container_group/:id/jobs">
<JobList

View File

@ -11,7 +11,7 @@ import { jsonToYaml, isJsonString } from 'util/yaml';
import ContainerGroupForm from '../shared/ContainerGroupForm';
function ContainerGroupAdd({ defaultExecution, defaultControlPlane }) {
function ContainerGroupAdd() {
const history = useHistory();
const [submitError, setSubmitError] = useState(null);
@ -93,8 +93,6 @@ function ContainerGroupAdd({ defaultExecution, defaultControlPlane }) {
<Card>
<CardBody>
<ContainerGroupForm
defaultControlPlane={defaultControlPlane}
defaultExecution={defaultExecution}
initialPodSpec={initialPodSpec}
onSubmit={handleSubmit}
submitError={submitError}

View File

@ -15,7 +15,7 @@ import { jsonToYaml, isJsonString } from 'util/yaml';
import { InstanceGroupsAPI } from 'api';
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
function ContainerGroupDetails({ instanceGroup, defaultExecution }) {
function ContainerGroupDetails({ instanceGroup }) {
const { id, name } = instanceGroup;
const history = useHistory();
@ -99,8 +99,7 @@ function ContainerGroupDetails({ instanceGroup, defaultExecution }) {
{t`Edit`}
</Button>
)}
{name !== defaultExecution &&
instanceGroup.summary_fields.user_capabilities &&
{instanceGroup.summary_fields.user_capabilities &&
instanceGroup.summary_fields.user_capabilities.delete && (
<DeleteButton
ouiaId="container-group-detail-delete-button"

View File

@ -9,11 +9,7 @@ import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading';
import ContainerGroupForm from '../shared/ContainerGroupForm';
function ContainerGroupEdit({
instanceGroup,
defaultControlPlane,
defaultExecution,
}) {
function ContainerGroupEdit({ instanceGroup }) {
const history = useHistory();
const [submitError, setSubmitError] = useState(null);
const detailsIUrl = `/instance_groups/container_group/${instanceGroup.id}/details`;
@ -77,8 +73,6 @@ function ContainerGroupEdit({
return (
<CardBody>
<ContainerGroupForm
defaultControlPlane={defaultControlPlane}
defaultExecution={defaultExecution}
instanceGroup={instanceGroup}
initialPodSpec={initialPodSpec}
onSubmit={handleSubmit}

View File

@ -13,7 +13,7 @@ import { CaretLeftIcon } from '@patternfly/react-icons';
import { Card, PageSection } from '@patternfly/react-core';
import useRequest from 'hooks/useRequest';
import { InstanceGroupsAPI, SettingsAPI } from 'api';
import { InstanceGroupsAPI } from 'api';
import RoutedTabs from 'components/RoutedTabs';
import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading';
@ -31,29 +31,16 @@ function InstanceGroup({ setBreadcrumb }) {
isLoading,
error: contentError,
request: fetchInstanceGroups,
result: { instanceGroup, defaultControlPlane, defaultExecution },
result: { instanceGroup },
} = useRequest(
useCallback(async () => {
const [
{ data },
{
data: {
DEFAULT_CONTROL_PLANE_QUEUE_NAME,
DEFAULT_EXECUTION_QUEUE_NAME,
},
},
] = await Promise.all([
InstanceGroupsAPI.readDetail(id),
SettingsAPI.readAll(),
]);
const { data } = await InstanceGroupsAPI.readDetail(id);
return {
instanceGroup: data,
defaultControlPlane: DEFAULT_CONTROL_PLANE_QUEUE_NAME,
defaultExecution: DEFAULT_EXECUTION_QUEUE_NAME,
};
}, [id]),
{ instanceGroup: null, defaultControlPlane: '', defaultExecution: '' }
{ instanceGroup: null }
);
useEffect(() => {
@ -133,18 +120,10 @@ function InstanceGroup({ setBreadcrumb }) {
{instanceGroup && (
<>
<Route path="/instance_groups/:id/edit">
<InstanceGroupEdit
instanceGroup={instanceGroup}
defaultExecution={defaultExecution}
defaultControlPlane={defaultControlPlane}
/>
<InstanceGroupEdit instanceGroup={instanceGroup} />
</Route>
<Route path="/instance_groups/:id/details">
<InstanceGroupDetails
defaultExecution={defaultExecution}
defaultControlPlane={defaultControlPlane}
instanceGroup={instanceGroup}
/>
<InstanceGroupDetails instanceGroup={instanceGroup} />
</Route>
<Route path="/instance_groups/:id/instances">
<Instances

View File

@ -6,7 +6,7 @@ import { CardBody } from 'components/Card';
import { InstanceGroupsAPI } from 'api';
import InstanceGroupForm from '../shared/InstanceGroupForm';
function InstanceGroupAdd({ defaultExecution, defaultControlPlane }) {
function InstanceGroupAdd() {
const history = useHistory();
const [submitError, setSubmitError] = useState(null);
@ -28,8 +28,6 @@ function InstanceGroupAdd({ defaultExecution, defaultControlPlane }) {
<Card>
<CardBody>
<InstanceGroupForm
defaultControlPlane={defaultControlPlane}
defaultExecution={defaultExecution}
onSubmit={handleSubmit}
submitError={submitError}
onCancel={handleCancel}

View File

@ -23,11 +23,7 @@ const Unavailable = styled.span`
color: var(--pf-global--danger-color--200);
`;
function InstanceGroupDetails({
instanceGroup,
defaultControlPlane,
defaultExecution,
}) {
function InstanceGroupDetails({ instanceGroup }) {
const { id, name } = instanceGroup;
const history = useHistory();
@ -46,8 +42,6 @@ function InstanceGroupDetails({
const { error, dismissError } = useDismissableError(deleteError);
const deleteDetailsRequests =
relatedResourceDeleteRequests.instanceGroup(instanceGroup);
const isDefaultInstanceGroup =
name === defaultControlPlane || name === defaultExecution;
return (
<CardBody>
<DetailList>
@ -115,8 +109,7 @@ function InstanceGroupDetails({
{t`Edit`}
</Button>
)}
{!isDefaultInstanceGroup &&
instanceGroup.summary_fields.user_capabilities &&
{instanceGroup.summary_fields.user_capabilities &&
instanceGroup.summary_fields.user_capabilities.delete && (
<DeleteButton
ouiaId="instance-group-detail-delete-button"

View File

@ -5,11 +5,7 @@ import { CardBody } from 'components/Card';
import { InstanceGroupsAPI } from 'api';
import InstanceGroupForm from '../shared/InstanceGroupForm';
function InstanceGroupEdit({
instanceGroup,
defaultControlPlane,
defaultExecution,
}) {
function InstanceGroupEdit({ instanceGroup }) {
const history = useHistory();
const [submitError, setSubmitError] = useState(null);
const detailsUrl = `/instance_groups/${instanceGroup.id}/details`;
@ -31,8 +27,6 @@ function InstanceGroupEdit({
<CardBody>
<InstanceGroupForm
instanceGroup={instanceGroup}
defaultControlPlane={defaultControlPlane}
defaultExecution={defaultExecution}
onSubmit={handleSubmit}
submitError={submitError}
onCancel={handleCancel}

View File

@ -55,10 +55,7 @@ describe('<InstanceGroupEdit>', () => {
history = createMemoryHistory();
await act(async () => {
wrapper = mountWithContexts(
<InstanceGroupEdit
defaultControlPlane="controlplane"
instanceGroup={instanceGroupData}
/>,
<InstanceGroupEdit instanceGroup={instanceGroupData} />,
{
context: { router: { history } },
}
@ -70,27 +67,6 @@ describe('<InstanceGroupEdit>', () => {
jest.clearAllMocks();
});
test('controlplane instance group name can not be updated', async () => {
let towerWrapper;
await act(async () => {
towerWrapper = mountWithContexts(
<InstanceGroupEdit
defaultControlPlane="controlplane"
instanceGroup={{ ...instanceGroupData, name: 'controlplane' }}
/>,
{
context: { router: { history } },
}
);
});
expect(
towerWrapper.find('input#instance-group-name').prop('disabled')
).toBeTruthy();
expect(
towerWrapper.find('input#instance-group-name').prop('value')
).toEqual('controlplane');
});
test('handleSubmit should call the api and redirect to details page', async () => {
await act(async () => {
wrapper.find('InstanceGroupForm').invoke('onSubmit')(

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, SettingsAPI } from 'api';
import { InstanceGroupsAPI } from 'api';
import { getQSConfig, parseQueryString } from 'util/qs';
import useRequest, { useDeleteItems } from 'hooks/useRequest';
import useSelected from 'hooks/useSelected';
@ -27,28 +27,6 @@ const QS_CONFIG = getQSConfig('instance-group', {
page_size: 20,
});
function modifyInstanceGroups(
defaultControlPlane,
defaultExecution,
items = []
) {
return items.map((item) => {
const clonedItem = {
...item,
summary_fields: {
...item.summary_fields,
user_capabilities: {
...item.summary_fields.user_capabilities,
},
},
};
if (clonedItem.name === (defaultControlPlane || defaultExecution)) {
clonedItem.summary_fields.user_capabilities.delete = false;
}
return clonedItem;
});
}
function InstanceGroupList({
isKubernetes,
isSettingsRequestLoading,
@ -56,30 +34,6 @@ function InstanceGroupList({
}) {
const location = useLocation();
const match = useRouteMatch();
const {
error: protectedItemsError,
isLoading: isLoadingProtectedItems,
request: fetchProtectedItems,
result: { defaultControlPlane, defaultExecution },
} = useRequest(
useCallback(async () => {
const {
data: {
DEFAULT_CONTROL_PLANE_QUEUE_NAME,
DEFAULT_EXECUTION_QUEUE_NAME,
},
} = await SettingsAPI.readAll();
return {
defaultControlPlane: DEFAULT_CONTROL_PLANE_QUEUE_NAME,
defaultExecution: DEFAULT_EXECUTION_QUEUE_NAME,
};
}, []),
{ defaultControlPlane: '', defaultExecution: '' }
);
useEffect(() => {
fetchProtectedItems();
}, [fetchProtectedItems]);
const {
error: contentError,
@ -127,12 +81,6 @@ function InstanceGroupList({
const { selected, isAllSelected, handleSelect, clearSelected, selectAll } =
useSelected(instanceGroups);
const modifiedSelected = modifyInstanceGroups(
defaultControlPlane,
defaultExecution,
selected
);
const {
isLoading: deleteLoading,
deletionError,
@ -158,28 +106,10 @@ function InstanceGroupList({
const canAdd = actions && actions.POST;
const cannotDelete = (item) =>
!item.summary_fields.user_capabilities.delete ||
item.name === defaultExecution ||
item.name === defaultControlPlane;
const cannotDelete = (item) => !item.summary_fields.user_capabilities.delete;
const pluralizedItemName = t`Instance Groups`;
let errorMessageDelete = '';
const notdeletedable = selected.filter(
(i) => i.name === defaultControlPlane || i.name === defaultExecution
);
if (notdeletedable.length) {
errorMessageDelete = (
<Plural
value={notdeletedable.length}
one="The following Instance Group cannot be deleted"
other="The following Instance Groups cannot be deleted"
/>
);
}
const addContainerGroup = t`Add container group`;
const addInstanceGroup = t`Add instance group`;
@ -229,14 +159,9 @@ function InstanceGroupList({
<PageSection>
<Card>
<PaginatedTable
contentError={
contentError || settingsRequestError || protectedItemsError
}
contentError={contentError || settingsRequestError}
hasContentLoading={
isLoading ||
deleteLoading ||
isSettingsRequestLoading ||
isLoadingProtectedItems
isLoading || deleteLoading || isSettingsRequestLoading
}
items={instanceGroups}
itemCount={instanceGroupsCount}
@ -264,9 +189,8 @@ function InstanceGroupList({
key="delete"
onDelete={handleDelete}
cannotDelete={cannotDelete}
itemsToDelete={modifiedSelected}
itemsToDelete={selected}
pluralizedItemName={t`Instance Groups`}
errorMessage={errorMessageDelete}
deleteDetailsRequests={deleteDetailsRequests}
deleteMessage={
<Plural

View File

@ -4,6 +4,7 @@ import { t } from '@lingui/macro';
import { Route, Switch, useLocation } from 'react-router-dom';
import { Card, PageSection } from '@patternfly/react-core';
import { useUserProfile } from 'contexts/Config';
import useRequest from 'hooks/useRequest';
import { SettingsAPI } from 'api';
import ScreenHeader from 'components/ScreenHeader';
@ -16,31 +17,28 @@ import ContainerGroup from './ContainerGroup';
function InstanceGroups() {
const { pathname } = useLocation();
const { isSuperUser, isSystemAuditor } = useUserProfile();
const userCanReadSettings = isSuperUser || isSystemAuditor;
const {
request: settingsRequest,
isLoading: isSettingsRequestLoading,
error: settingsRequestError,
result: { isKubernetes, defaultControlPlane, defaultExecution },
result: { isKubernetes },
} = useRequest(
useCallback(async () => {
const {
data: {
IS_K8S,
DEFAULT_CONTROL_PLANE_QUEUE_NAME,
DEFAULT_EXECUTION_QUEUE_NAME,
},
data: { IS_K8S },
} = await SettingsAPI.readCategory('all');
return {
isKubernetes: IS_K8S,
defaultControlPlane: DEFAULT_CONTROL_PLANE_QUEUE_NAME,
defaultExecution: DEFAULT_EXECUTION_QUEUE_NAME,
};
}, []),
{ isLoading: true }
{ isKubernetes: false }
);
useEffect(() => {
settingsRequest();
}, [settingsRequest]);
userCanReadSettings && settingsRequest();
}, [settingsRequest, userCanReadSettings]);
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
'/instance_groups': t`Instance Groups`,
@ -91,20 +89,14 @@ function InstanceGroups() {
) : (
<Switch>
<Route path="/instance_groups/container_group/add">
<ContainerGroupAdd
defaultControlPlane={defaultControlPlane}
defaultExecution={defaultExecution}
/>
<ContainerGroupAdd />
</Route>
<Route path="/instance_groups/container_group/:id">
<ContainerGroup setBreadcrumb={buildBreadcrumbConfig} />
</Route>
{!isKubernetes && (
<Route path="/instance_groups/add">
<InstanceGroupAdd
defaultControlPlane={defaultControlPlane}
defaultExecution={defaultExecution}
/>
<InstanceGroupAdd />
</Route>
)}
<Route path="/instance_groups/:id">

View File

@ -2,6 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { InstanceGroupsAPI } from 'api';
import InstanceGroups from './InstanceGroups';
import { useUserProfile } from 'contexts/Config';
const mockUseLocationValue = {
pathname: '',
@ -11,6 +12,19 @@ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => mockUseLocationValue,
}));
beforeEach(() => {
useUserProfile.mockImplementation(() => {
return {
isSuperUser: true,
isSystemAuditor: false,
isOrgAdmin: false,
isNotificationAdmin: false,
isExecEnvAdmin: false,
};
});
});
describe('<InstanceGroups/>', () => {
test('should set breadcrumbs', () => {
mockUseLocationValue.pathname = '/instance_groups';

View File

@ -11,7 +11,7 @@ import FormField, {
CheckboxField,
} from 'components/FormField';
import FormActionGroup from 'components/FormActionGroup';
import { combine, required, protectedResourceName } from 'util/validators';
import { required } from 'util/validators';
import {
FormColumnLayout,
FormFullWidthLayout,
@ -21,21 +21,11 @@ import {
import CredentialLookup from 'components/Lookup/CredentialLookup';
import { VariablesField } from 'components/CodeEditor';
function ContainerGroupFormFields({
instanceGroup,
defaultControlPlane,
defaultExecution,
}) {
function ContainerGroupFormFields({ instanceGroup }) {
const { setFieldValue, setFieldTouched } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] =
useField('credential');
const [, { initialValue }] = useField('name');
const isProtected =
initialValue === `${defaultControlPlane}` ||
initialValue === `${defaultExecution}`;
const [overrideField] = useField('override');
const handleCredentialUpdate = useCallback(
@ -50,21 +40,10 @@ function ContainerGroupFormFields({
<>
<FormField
name="name"
helperText={
isProtected
? t`This is a protected Instance Group. The name cannot be changed.`
: ''
}
id="container-group-name"
label={t`Name`}
type="text"
validate={combine([
required(null),
protectedResourceName(
t`This is a protected name for Container Groups. Please use a different name.`,
[defaultControlPlane, defaultExecution]
),
])}
validate={required(null)}
isRequired
/>
<CredentialLookup

View File

@ -1,49 +1,25 @@
import React from 'react';
import { func, shape } from 'prop-types';
import { Formik, useField } from 'formik';
import { Formik } from 'formik';
import { t } from '@lingui/macro';
import { Form } from '@patternfly/react-core';
import FormField, { FormSubmitError } from 'components/FormField';
import FormActionGroup from 'components/FormActionGroup';
import {
combine,
required,
protectedResourceName,
minMaxValue,
} from 'util/validators';
import { required, minMaxValue } from 'util/validators';
import { FormColumnLayout } from 'components/FormLayout';
function InstanceGroupFormFields({ defaultControlPlane, defaultExecution }) {
const [, { initialValue }] = useField('name');
const isProtected =
initialValue === `${defaultControlPlane}` ||
initialValue === `${defaultExecution}`;
const validators = combine([
required(null),
protectedResourceName(
t`This is a protected name for Instance Groups. Please use a different name.`,
[defaultControlPlane, defaultExecution]
),
]);
function InstanceGroupFormFields() {
return (
<>
<FormField
name="name"
helperText={
isProtected
? t`This is a protected Instance Group. The name cannot be changed.`
: ''
}
id="instance-group-name"
label={t`Name`}
type="text"
validate={validators}
validate={required(null)}
isRequired
isDisabled={isProtected}
/>
<FormField
id="instance-group-policy-instance-minimum"

View File

@ -117,44 +117,4 @@ describe('<InstanceGroupForm/>', () => {
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
expect(onCancel).toBeCalled();
});
test('Name field should be disabled, default', async () => {
let defaultInstanceGroupWrapper;
await act(async () => {
defaultInstanceGroupWrapper = mountWithContexts(
<InstanceGroupForm
onCancel={onCancel}
onSubmit={onSubmit}
defaultControlPlane="controlplane"
defaultExecution="default"
instanceGroup={{ ...instanceGroup, name: 'default' }}
/>
);
});
expect(
defaultInstanceGroupWrapper
.find('TextInput[name="name"]')
.prop('isDisabled')
).toBe(true);
});
test('Name field should be disabled, controlplane', async () => {
let defaultInstanceGroupWrapper;
await act(async () => {
defaultInstanceGroupWrapper = mountWithContexts(
<InstanceGroupForm
onCancel={onCancel}
onSubmit={onSubmit}
defaultControlPlane="controlplane"
defaultExecution="default"
instanceGroup={{ ...instanceGroup, name: 'controlplane' }}
/>
);
});
expect(
defaultInstanceGroupWrapper
.find('TextInput[name="name"]')
.prop('isDisabled')
).toBe(true);
});
});

View File

@ -93,6 +93,7 @@ jest.doMock('./contexts/Config', () => ({
Config: MockConfigContext.Consumer,
useConfig: () => React.useContext(MockConfigContext),
useAuthorizedPath: jest.fn(),
useUserProfile: jest.fn(),
}));
// ?