mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 01:57:35 -03:30
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:
parent
2e4d866f69
commit
ce8b9750c9
@ -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)
|
||||
|
||||
@ -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'))
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
"""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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')(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -93,6 +93,7 @@ jest.doMock('./contexts/Config', () => ({
|
||||
Config: MockConfigContext.Consumer,
|
||||
useConfig: () => React.useContext(MockConfigContext),
|
||||
useAuthorizedPath: jest.fn(),
|
||||
useUserProfile: jest.fn(),
|
||||
}));
|
||||
|
||||
// ?
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user