mirror of
https://github.com/ansible/awx.git
synced 2026-05-19 14:57:39 -02:30
prevent delete of instance groups list item controlplan and default
This commit is contained in:
committed by
Shane McDonald
parent
f541fe9904
commit
04839a037a
@@ -14,6 +14,10 @@ class Settings extends Base {
|
|||||||
return this.http.patch(`${this.baseUrl}all/`, data);
|
return this.http.patch(`${this.baseUrl}all/`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readAll() {
|
||||||
|
return this.http.get(`${this.baseUrl}all/`);
|
||||||
|
}
|
||||||
|
|
||||||
updateCategory(category, data) {
|
updateCategory(category, data) {
|
||||||
return this.http.patch(`${this.baseUrl}${category}/`, data);
|
return this.http.patch(`${this.baseUrl}${category}/`, data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { CaretLeftIcon } from '@patternfly/react-icons';
|
|||||||
import { Card, PageSection } from '@patternfly/react-core';
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
|
|
||||||
import useRequest from '../../util/useRequest';
|
import useRequest from '../../util/useRequest';
|
||||||
import { InstanceGroupsAPI } from '../../api';
|
import { InstanceGroupsAPI, SettingsAPI } from '../../api';
|
||||||
import RoutedTabs from '../../components/RoutedTabs';
|
import RoutedTabs from '../../components/RoutedTabs';
|
||||||
import ContentError from '../../components/ContentError';
|
import ContentError from '../../components/ContentError';
|
||||||
import ContentLoading from '../../components/ContentLoading';
|
import ContentLoading from '../../components/ContentLoading';
|
||||||
@@ -31,12 +31,28 @@ function InstanceGroup({ setBreadcrumb }) {
|
|||||||
isLoading,
|
isLoading,
|
||||||
error: contentError,
|
error: contentError,
|
||||||
request: fetchInstanceGroups,
|
request: fetchInstanceGroups,
|
||||||
result: instanceGroup,
|
result: { instanceGroup, defaultControlPlane, defaultExecution },
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const { data } = await InstanceGroupsAPI.readDetail(id);
|
const [
|
||||||
return data;
|
{ data },
|
||||||
}, [id])
|
{
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
@@ -115,7 +131,11 @@ function InstanceGroup({ setBreadcrumb }) {
|
|||||||
{instanceGroup && (
|
{instanceGroup && (
|
||||||
<>
|
<>
|
||||||
<Route path="/instance_groups/:id/edit">
|
<Route path="/instance_groups/:id/edit">
|
||||||
<InstanceGroupEdit instanceGroup={instanceGroup} />
|
<InstanceGroupEdit
|
||||||
|
instanceGroup={instanceGroup}
|
||||||
|
defaultExecution={defaultExecution}
|
||||||
|
defaultControlPlane={defaultControlPlane}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/instance_groups/:id/details">
|
<Route path="/instance_groups/:id/details">
|
||||||
<InstanceGroupDetails instanceGroup={instanceGroup} />
|
<InstanceGroupDetails instanceGroup={instanceGroup} />
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { CardBody } from '../../../components/Card';
|
|||||||
import { InstanceGroupsAPI } from '../../../api';
|
import { InstanceGroupsAPI } from '../../../api';
|
||||||
import InstanceGroupForm from '../shared/InstanceGroupForm';
|
import InstanceGroupForm from '../shared/InstanceGroupForm';
|
||||||
|
|
||||||
function InstanceGroupEdit({ instanceGroup }) {
|
function InstanceGroupEdit({
|
||||||
|
instanceGroup,
|
||||||
|
defaultExecution,
|
||||||
|
defaultControlPlane,
|
||||||
|
}) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [submitError, setSubmitError] = useState(null);
|
const [submitError, setSubmitError] = useState(null);
|
||||||
const detailsUrl = `/instance_groups/${instanceGroup.id}/details`;
|
const detailsUrl = `/instance_groups/${instanceGroup.id}/details`;
|
||||||
@@ -27,6 +31,8 @@ function InstanceGroupEdit({ instanceGroup }) {
|
|||||||
<CardBody>
|
<CardBody>
|
||||||
<InstanceGroupForm
|
<InstanceGroupForm
|
||||||
instanceGroup={instanceGroup}
|
instanceGroup={instanceGroup}
|
||||||
|
defaultExecution={defaultExecution}
|
||||||
|
defaultControlPlane={defaultControlPlane}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
submitError={submitError}
|
submitError={submitError}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ describe('<InstanceGroupEdit>', () => {
|
|||||||
history = createMemoryHistory();
|
history = createMemoryHistory();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<InstanceGroupEdit instanceGroup={instanceGroupData} />,
|
<InstanceGroupEdit
|
||||||
|
defaultExecution="default"
|
||||||
|
defaultControlPlane="controlplane"
|
||||||
|
instanceGroup={instanceGroupData}
|
||||||
|
/>,
|
||||||
{
|
{
|
||||||
context: { router: { history } },
|
context: { router: { history } },
|
||||||
}
|
}
|
||||||
@@ -68,12 +72,14 @@ describe('<InstanceGroupEdit>', () => {
|
|||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tower instance group name can not be updated', async () => {
|
test('controlplane instance group name can not be updated', async () => {
|
||||||
let towerWrapper;
|
let towerWrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
towerWrapper = mountWithContexts(
|
towerWrapper = mountWithContexts(
|
||||||
<InstanceGroupEdit
|
<InstanceGroupEdit
|
||||||
instanceGroup={{ ...instanceGroupData, name: 'tower' }}
|
defaultExecution="default"
|
||||||
|
defaultControlPlane="controlplane"
|
||||||
|
instanceGroup={{ ...instanceGroupData, name: 'controlplane' }}
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
context: { router: { history } },
|
context: { router: { history } },
|
||||||
@@ -85,7 +91,29 @@ describe('<InstanceGroupEdit>', () => {
|
|||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
towerWrapper.find('input#instance-group-name').prop('value')
|
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 () => {
|
test('handleSubmit should call the api and redirect to details page', async () => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useLocation, useRouteMatch, Link } from 'react-router-dom';
|
|||||||
import { t, Plural } from '@lingui/macro';
|
import { t, Plural } from '@lingui/macro';
|
||||||
import { Card, PageSection, DropdownItem } from '@patternfly/react-core';
|
import { Card, PageSection, DropdownItem } from '@patternfly/react-core';
|
||||||
|
|
||||||
import { InstanceGroupsAPI } from '../../../api';
|
import { InstanceGroupsAPI, SettingsAPI } from '../../../api';
|
||||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||||
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
||||||
import useSelected from '../../../util/useSelected';
|
import useSelected from '../../../util/useSelected';
|
||||||
@@ -26,7 +26,11 @@ const QS_CONFIG = getQSConfig('instance-group', {
|
|||||||
page_size: 20,
|
page_size: 20,
|
||||||
});
|
});
|
||||||
|
|
||||||
function modifyInstanceGroups(items = []) {
|
function modifyInstanceGroups(
|
||||||
|
items = [],
|
||||||
|
defaultControlPlane,
|
||||||
|
defaultExecution
|
||||||
|
) {
|
||||||
return items.map(item => {
|
return items.map(item => {
|
||||||
const clonedItem = {
|
const clonedItem = {
|
||||||
...item,
|
...item,
|
||||||
@@ -37,7 +41,7 @@ function modifyInstanceGroups(items = []) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (clonedItem.name === 'tower') {
|
if (clonedItem.name === (defaultControlPlane || defaultExecution)) {
|
||||||
clonedItem.summary_fields.user_capabilities.delete = false;
|
clonedItem.summary_fields.user_capabilities.delete = false;
|
||||||
}
|
}
|
||||||
return clonedItem;
|
return clonedItem;
|
||||||
@@ -62,18 +66,32 @@ function InstanceGroupList({
|
|||||||
actions,
|
actions,
|
||||||
relatedSearchableKeys,
|
relatedSearchableKeys,
|
||||||
searchableKeys,
|
searchableKeys,
|
||||||
|
defaultControlPlane,
|
||||||
|
defaultExecution,
|
||||||
},
|
},
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
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.read(params),
|
||||||
InstanceGroupsAPI.readOptions(),
|
InstanceGroupsAPI.readOptions(),
|
||||||
|
SettingsAPI.readAll(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
instanceGroups: response.data.results,
|
instanceGroups: response.data.results,
|
||||||
|
defaultControlPlane: DEFAULT_CONTROL_PLANE_QUEUE_NAME,
|
||||||
|
defaultExecution: DEFAULT_EXECUTION_QUEUE_NAME,
|
||||||
instanceGroupsCount: response.data.count,
|
instanceGroupsCount: response.data.count,
|
||||||
actions: responseActions.data.actions,
|
actions: responseActions.data.actions,
|
||||||
relatedSearchableKeys: (
|
relatedSearchableKeys: (
|
||||||
@@ -105,7 +123,11 @@ function InstanceGroupList({
|
|||||||
selectAll,
|
selectAll,
|
||||||
} = useSelected(instanceGroups);
|
} = useSelected(instanceGroups);
|
||||||
|
|
||||||
const modifiedSelected = modifyInstanceGroups(selected);
|
const modifiedSelected = modifyInstanceGroups(
|
||||||
|
selected,
|
||||||
|
defaultControlPlane,
|
||||||
|
defaultExecution
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isLoading: deleteLoading,
|
isLoading: deleteLoading,
|
||||||
@@ -133,31 +155,25 @@ function InstanceGroupList({
|
|||||||
const canAdd = actions && actions.POST;
|
const canAdd = actions && actions.POST;
|
||||||
|
|
||||||
function cannotDelete(item) {
|
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`;
|
const pluralizedItemName = t`Instance Groups`;
|
||||||
|
|
||||||
let errorMessageDelete = '';
|
let errorMessageDelete = '';
|
||||||
|
|
||||||
if (modifiedSelected.some(item => item.name === 'tower')) {
|
if (
|
||||||
const itemsUnableToDelete = modifiedSelected
|
modifiedSelected.some(
|
||||||
.filter(cannotDelete)
|
item =>
|
||||||
.filter(item => item.name !== 'tower')
|
item.name === defaultControlPlane || item.name === defaultExecution
|
||||||
.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');
|
|
||||||
}
|
|
||||||
errorMessageDelete = errorMessageDelete.concat(
|
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
|
<ToolbarDeleteButton
|
||||||
key="delete"
|
key="delete"
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
|
cannotDelete={cannotDelete}
|
||||||
itemsToDelete={modifiedSelected}
|
itemsToDelete={modifiedSelected}
|
||||||
pluralizedItemName={t`Instance Groups`}
|
pluralizedItemName={t`Instance Groups`}
|
||||||
errorMessage={errorMessageDelete}
|
errorMessage={errorMessageDelete}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
OrganizationsAPI,
|
OrganizationsAPI,
|
||||||
InventoriesAPI,
|
InventoriesAPI,
|
||||||
UnifiedJobTemplatesAPI,
|
UnifiedJobTemplatesAPI,
|
||||||
|
SettingsAPI,
|
||||||
} from '../../../api';
|
} from '../../../api';
|
||||||
import InstanceGroupList from './InstanceGroupList';
|
import InstanceGroupList from './InstanceGroupList';
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ jest.mock('../../../api/models/InstanceGroups');
|
|||||||
jest.mock('../../../api/models/Organizations');
|
jest.mock('../../../api/models/Organizations');
|
||||||
jest.mock('../../../api/models/Inventories');
|
jest.mock('../../../api/models/Inventories');
|
||||||
jest.mock('../../../api/models/UnifiedJobTemplates');
|
jest.mock('../../../api/models/UnifiedJobTemplates');
|
||||||
|
jest.mock('../../../api/models/Settings');
|
||||||
|
|
||||||
const instanceGroups = {
|
const instanceGroups = {
|
||||||
data: {
|
data: {
|
||||||
@@ -32,7 +34,7 @@ const instanceGroups = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'tower',
|
name: 'controlplan',
|
||||||
type: 'instance_group',
|
type: 'instance_group',
|
||||||
url: '/api/v2/instance_groups/2',
|
url: '/api/v2/instance_groups/2',
|
||||||
consumed_capacity: 42,
|
consumed_capacity: 42,
|
||||||
@@ -40,6 +42,14 @@ const instanceGroups = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
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',
|
name: 'Bar',
|
||||||
type: 'instance_group',
|
type: 'instance_group',
|
||||||
url: '/api/v2/instance_groups/3',
|
url: '/api/v2/instance_groups/3',
|
||||||
@@ -47,11 +57,17 @@ const instanceGroups = {
|
|||||||
summary_fields: { user_capabilities: { edit: true, delete: false } },
|
summary_fields: { user_capabilities: { edit: true, delete: false } },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
count: 3,
|
count: 4,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = { data: { actions: { POST: true } } };
|
const options = { data: { actions: { POST: true } } };
|
||||||
|
const settings = {
|
||||||
|
data: {
|
||||||
|
DEFAULT_CONTROL_PLANE_QUEUE_NAME: 'controlplan',
|
||||||
|
DEFAULT_EXECUTION_QUEUE_NAME: 'default',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
describe('<InstanceGroupList />', () => {
|
describe('<InstanceGroupList />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
@@ -62,6 +78,7 @@ describe('<InstanceGroupList />', () => {
|
|||||||
UnifiedJobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
UnifiedJobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
InstanceGroupsAPI.read.mockResolvedValue(instanceGroups);
|
InstanceGroupsAPI.read.mockResolvedValue(instanceGroups);
|
||||||
InstanceGroupsAPI.readOptions.mockResolvedValue(options);
|
InstanceGroupsAPI.readOptions.mockResolvedValue(options);
|
||||||
|
SettingsAPI.readAll.mockResolvedValue(settings);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should have data fetched and render 3 rows', async () => {
|
test('should have data fetched and render 3 rows', async () => {
|
||||||
@@ -69,7 +86,7 @@ describe('<InstanceGroupList />', () => {
|
|||||||
wrapper = mountWithContexts(<InstanceGroupList />);
|
wrapper = mountWithContexts(<InstanceGroupList />);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'InstanceGroupList', el => el.length > 0);
|
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.read).toBeCalled();
|
||||||
expect(InstanceGroupsAPI.readOptions).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 () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<InstanceGroupList />);
|
wrapper = mountWithContexts(<InstanceGroupList />);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'InstanceGroupList', el => el.length > 0);
|
await waitForElement(wrapper, 'InstanceGroupList', el => el.length > 0);
|
||||||
|
|
||||||
const instanceGroupIndex = [0, 1, 2];
|
const instanceGroupIndex = [0, 1, 2, 3];
|
||||||
|
|
||||||
instanceGroupIndex.forEach(element => {
|
instanceGroupIndex.forEach(element => {
|
||||||
wrapper
|
wrapper
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import FormActionGroup from '../../../components/FormActionGroup';
|
|||||||
import { required, minMaxValue } from '../../../util/validators';
|
import { required, minMaxValue } from '../../../util/validators';
|
||||||
import { FormColumnLayout } from '../../../components/FormLayout';
|
import { FormColumnLayout } from '../../../components/FormLayout';
|
||||||
|
|
||||||
function InstanceGroupFormFields() {
|
function InstanceGroupFormFields({ defaultExecution, defaultControlPlane }) {
|
||||||
const [instanceGroupNameField, ,] = useField('name');
|
const [instanceGroupNameField, ,] = useField('name');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -21,7 +22,10 @@ function InstanceGroupFormFields() {
|
|||||||
type="text"
|
type="text"
|
||||||
validate={required(null)}
|
validate={required(null)}
|
||||||
isRequired
|
isRequired
|
||||||
isDisabled={instanceGroupNameField.value === 'tower'}
|
isDisabled={
|
||||||
|
instanceGroupNameField.value === defaultExecution ||
|
||||||
|
instanceGroupNameField.value === defaultControlPlane
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
id="instance-group-policy-instance-minimum"
|
id="instance-group-policy-instance-minimum"
|
||||||
@@ -50,6 +54,7 @@ function InstanceGroupFormFields() {
|
|||||||
|
|
||||||
function InstanceGroupForm({
|
function InstanceGroupForm({
|
||||||
instanceGroup = {},
|
instanceGroup = {},
|
||||||
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
submitError,
|
submitError,
|
||||||
|
|||||||
Reference in New Issue
Block a user