Add order selected list to instance group lookups

This commit is contained in:
Marliana Lara 2021-06-24 14:16:02 -04:00 committed by Shane McDonald
parent f1273d5810
commit 162ea776fd
No known key found for this signature in database
GPG Key ID: 6F374AF6E9EB9374
16 changed files with 129 additions and 109 deletions

View File

@ -1,3 +1,12 @@
function isEqual(array1, array2) {
return (
array1.length === array2.length &&
array1.every((element, index) => {
return element.id === array2[index].id;
})
);
}
const InstanceGroupsMixin = parent =>
class extends parent {
readInstanceGroups(resourceId, params) {
@ -18,6 +27,19 @@ const InstanceGroupsMixin = parent =>
disassociate: true,
});
}
async orderInstanceGroups(resourceId, current, original) {
/* eslint-disable no-await-in-loop, no-restricted-syntax */
if (!isEqual(current, original)) {
for (const group of original) {
await this.disassociateInstanceGroup(resourceId, group.id);
}
for (const group of current) {
await this.associateInstanceGroup(resourceId, group.id);
}
}
}
/* eslint-enable no-await-in-loop, no-restricted-syntax */
};
export default InstanceGroupsMixin;

View File

@ -2,7 +2,7 @@ import React, { useCallback, useEffect } from 'react';
import { arrayOf, string, func, bool } from 'prop-types';
import { withRouter } from 'react-router-dom';
import { t } from '@lingui/macro';
import { t, Trans } from '@lingui/macro';
import { FormGroup } from '@patternfly/react-core';
import { InstanceGroupsAPI } from '../../api';
import { InstanceGroup } from '../../types';
@ -82,6 +82,18 @@ function InstanceGroupsLookup({
multiple
required={required}
isLoading={isLoading}
modalDescription={
<>
<b>
<Trans>Selected</Trans>
</b>
<br />
<Trans>
Note: The order in which these are selected sets the execution
precedence.
</Trans>
</>
}
renderOptionsList={({ state, dispatch, canDelete }) => (
<OptionsList
value={state.selectedItems}
@ -113,6 +125,10 @@ function InstanceGroupsLookup({
readOnly={!canDelete}
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
sortSelectedItems={selectedItems =>
dispatch({ type: 'SET_SELECTED_ITEMS', selectedItems })
}
isSelectedDraggable
/>
)}
/>

View File

@ -23,12 +23,11 @@ function InventoryAdd() {
organization: organization.id,
...remainingValues,
});
if (instanceGroups) {
const associatePromises = instanceGroups.map(async ig =>
InventoriesAPI.associateInstanceGroup(inventoryId, ig.id)
);
await Promise.all(associatePromises);
/* eslint-disable no-await-in-loop, no-restricted-syntax */
for (const group of instanceGroups) {
await InventoriesAPI.associateInstanceGroup(inventoryId, group.id);
}
/* eslint-enable no-await-in-loop, no-restricted-syntax */
const url = history.location.pathname.startsWith(
'/inventories/smart_inventory'
)

View File

@ -6,7 +6,6 @@ import { CardBody } from '../../../components/Card';
import { InventoriesAPI } from '../../../api';
import ContentLoading from '../../../components/ContentLoading';
import InventoryForm from '../shared/InventoryForm';
import { getAddedAndRemoved } from '../../../util/lists';
import useIsMounted from '../../../util/useIsMounted';
function InventoryEdit({ inventory }) {
@ -54,20 +53,12 @@ function InventoryEdit({ inventory }) {
organization: organization.id,
...remainingValues,
});
if (instanceGroups) {
const { added, removed } = getAddedAndRemoved(
associatedInstanceGroups,
instanceGroups
);
await InventoriesAPI.orderInstanceGroups(
inventory.id,
instanceGroups,
associatedInstanceGroups
);
const associatePromises = added.map(async ig =>
InventoriesAPI.associateInstanceGroup(inventory.id, ig.id)
);
const disassociatePromises = removed.map(async ig =>
InventoriesAPI.disassociateInstanceGroup(inventory.id, ig.id)
);
await Promise.all([...associatePromises, ...disassociatePromises]);
}
const url =
history.location.pathname.search('smart') > -1
? `/inventories/smart_inventory/${inventory.id}/details`

View File

@ -106,17 +106,10 @@ describe('<InventoryEdit />', () => {
});
});
await sleep(0);
instanceGroups.map(IG =>
expect(InventoriesAPI.associateInstanceGroup).toHaveBeenCalledWith(
1,
IG.id
)
);
associatedInstanceGroups.map(async aIG =>
expect(InventoriesAPI.disassociateInstanceGroup).toHaveBeenCalledWith(
1,
aIG.id
)
expect(InventoriesAPI.orderInstanceGroups).toHaveBeenCalledWith(
mockInventory.id,
instanceGroups,
associatedInstanceGroups
);
});
});

View File

@ -19,11 +19,11 @@ function SmartInventoryAdd() {
data: { id: invId },
} = await InventoriesAPI.create(values);
await Promise.all(
groupsToAssociate.map(({ id }) =>
InventoriesAPI.associateInstanceGroup(invId, id)
)
);
/* eslint-disable no-await-in-loop, no-restricted-syntax */
for (const group of groupsToAssociate) {
await InventoriesAPI.associateInstanceGroup(invId, group.id);
}
/* eslint-enable no-await-in-loop, no-restricted-syntax */
return invId;
}, [])
);

View File

@ -1,7 +1,6 @@
import React, { useCallback, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { Inventory } from '../../../types';
import { getAddedAndRemoved } from '../../../util/lists';
import useRequest from '../../../util/useRequest';
import { InventoriesAPI } from '../../../api';
import { CardBody } from '../../../components/Card';
@ -17,7 +16,7 @@ function SmartInventoryEdit({ inventory }) {
error: contentError,
isLoading: hasContentLoading,
request: fetchInstanceGroups,
result: instanceGroups,
result: initialInstanceGroups,
} = useRequest(
useCallback(async () => {
const {
@ -40,15 +39,10 @@ function SmartInventoryEdit({ inventory }) {
useCallback(
async (values, groupsToAssociate, groupsToDisassociate) => {
const { data } = await InventoriesAPI.update(inventory.id, values);
await Promise.all(
groupsToAssociate.map(id =>
InventoriesAPI.associateInstanceGroup(inventory.id, id)
)
);
await Promise.all(
groupsToDisassociate.map(id =>
InventoriesAPI.disassociateInstanceGroup(inventory.id, id)
)
await InventoriesAPI.orderInstanceGroups(
inventory.id,
groupsToAssociate,
groupsToDisassociate
);
return data;
},
@ -68,20 +62,13 @@ function SmartInventoryEdit({ inventory }) {
const handleSubmit = async form => {
const { instance_groups, organization, ...remainingForm } = form;
const { added, removed } = getAddedAndRemoved(
instanceGroups,
instance_groups
);
const addedIds = added.map(({ id }) => id);
const removedIds = removed.map(({ id }) => id);
await submitRequest(
{
organization: organization?.id,
...remainingForm,
},
addedIds,
removedIds
instance_groups,
initialInstanceGroups
);
};
@ -104,7 +91,7 @@ function SmartInventoryEdit({ inventory }) {
<CardBody>
<SmartInventoryForm
inventory={inventory}
instanceGroups={instanceGroups}
instanceGroups={initialInstanceGroups}
onCancel={handleCancel}
onSubmit={handleSubmit}
submitError={submitError}

View File

@ -104,8 +104,7 @@ describe('<SmartInventoryEdit />', () => {
});
});
expect(InventoriesAPI.update).toHaveBeenCalledTimes(1);
expect(InventoriesAPI.associateInstanceGroup).toHaveBeenCalledTimes(1);
expect(InventoriesAPI.disassociateInstanceGroup).toHaveBeenCalledTimes(1);
expect(InventoriesAPI.orderInstanceGroups).toHaveBeenCalledTimes(1);
});
test('successful form submission should trigger redirect to details', async () => {

View File

@ -41,13 +41,11 @@ function OrganizationAdd() {
...values,
default_environment: values.default_environment?.id,
});
await Promise.all(
groupsToAssociate.map(id =>
OrganizationsAPI.associateInstanceGroup(response.id, id)
)
);
/* eslint-disable no-await-in-loop, no-restricted-syntax */
// Resolve Promises sequentially to maintain order and avoid race condition
for (const group of groupsToAssociate) {
await OrganizationsAPI.associateInstanceGroup(response.id, group.id);
}
for (const credential of values.galaxy_credentials) {
await OrganizationsAPI.associateGalaxyCredential(
response.id,

View File

@ -95,6 +95,12 @@ describe('<OrganizationAdd />', () => {
description: 'new description',
galaxy_credentials: [],
};
const mockInstanceGroups = [
{
name: 'mock ig',
id: 3,
},
];
OrganizationsAPI.create.mockResolvedValueOnce({
data: {
id: 5,
@ -109,7 +115,10 @@ describe('<OrganizationAdd />', () => {
wrapper = mountWithContexts(<OrganizationAdd />);
});
await waitForElement(wrapper, 'button[aria-label="Save"]');
await wrapper.find('OrganizationForm').prop('onSubmit')(orgData, [3]);
await wrapper.find('OrganizationForm').prop('onSubmit')(
orgData,
mockInstanceGroups
);
expect(OrganizationsAPI.associateInstanceGroup).toHaveBeenCalledWith(5, 3);
});

View File

@ -26,16 +26,12 @@ function OrganizationEdit({ organization }) {
...values,
default_environment: values.default_environment?.id || null,
});
await Promise.all(
groupsToAssociate.map(id =>
OrganizationsAPI.associateInstanceGroup(organization.id, id)
)
);
await Promise.all(
groupsToDisassociate.map(id =>
OrganizationsAPI.disassociateInstanceGroup(organization.id, id)
)
await OrganizationsAPI.orderInstanceGroups(
organization.id,
groupsToAssociate,
groupsToDisassociate
);
/* eslint-disable no-await-in-loop, no-restricted-syntax */
// Resolve Promises sequentially to avoid race condition
if (

View File

@ -54,18 +54,34 @@ describe('<OrganizationEdit />', () => {
name: 'new name',
description: 'new description',
};
const newInstanceGroups = [
{
name: 'mock three',
id: 3,
},
{
name: 'mock four',
id: 4,
},
];
const oldInstanceGroups = [
{
name: 'mock two',
id: 2,
},
];
await act(async () => {
wrapper.find('OrganizationForm').invoke('onSubmit')(
updatedOrgData,
[3, 4],
[2]
newInstanceGroups,
oldInstanceGroups
);
});
expect(OrganizationsAPI.associateInstanceGroup).toHaveBeenCalledWith(1, 3);
expect(OrganizationsAPI.associateInstanceGroup).toHaveBeenCalledWith(1, 4);
expect(OrganizationsAPI.disassociateInstanceGroup).toHaveBeenCalledWith(
1,
2
expect(OrganizationsAPI.orderInstanceGroups).toHaveBeenCalledWith(
mockData.id,
newInstanceGroups,
oldInstanceGroups
);
});

View File

@ -15,7 +15,6 @@ import {
InstanceGroupsLookup,
ExecutionEnvironmentLookup,
} from '../../../components/Lookup';
import { getAddedAndRemoved } from '../../../util/lists';
import { required, minMaxValue } from '../../../util/validators';
import { FormColumnLayout } from '../../../components/FormLayout';
import CredentialLookup from '../../../components/Lookup/CredentialLookup';
@ -143,19 +142,13 @@ function OrganizationForm({
};
const handleSubmit = values => {
const { added, removed } = getAddedAndRemoved(
initialInstanceGroups,
instanceGroups
);
const addedIds = added.map(({ id }) => id);
const removedIds = removed.map(({ id }) => id);
if (
typeof values.max_hosts !== 'number' ||
values.max_hosts === 'undefined'
) {
values.max_hosts = 0;
}
onSubmit(values, addedIds, removedIds);
onSubmit(values, instanceGroups, initialInstanceGroups);
};
useEffect(() => {

View File

@ -258,7 +258,14 @@ describe('<OrganizationForm />', () => {
await act(async () => {
wrapper.find('button[aria-label="Save"]').simulate('click');
});
expect(onSubmit).toHaveBeenCalledWith(mockDataForm, [3], [2]);
expect(onSubmit).toHaveBeenCalledWith(
mockDataForm,
[
{ name: 'One', id: 1 },
{ name: 'Three', id: 3 },
],
mockInstanceGroups
);
});
test('onSubmit does not get called if max_hosts value is out of range', async () => {
@ -332,8 +339,8 @@ describe('<OrganizationForm />', () => {
max_hosts: 0,
default_environment: null,
},
[],
[]
mockInstanceGroups,
mockInstanceGroups
);
});

View File

@ -63,11 +63,12 @@ function JobTemplateAdd() {
return Promise.all([...associationPromises]);
}
function submitInstanceGroups(templateId, addedGroups = []) {
const associatePromises = addedGroups.map(group =>
JobTemplatesAPI.associateInstanceGroup(templateId, group.id)
);
return Promise.all(associatePromises);
async function submitInstanceGroups(templateId, addedGroups = []) {
/* eslint-disable no-await-in-loop, no-restricted-syntax */
for (const group of addedGroups) {
await JobTemplatesAPI.associateInstanceGroup(templateId, group.id);
}
/* eslint-enable no-await-in-loop, no-restricted-syntax */
}
function submitCredentials(templateId, credentials = []) {

View File

@ -61,8 +61,12 @@ function JobTemplateEdit({ template, reloadTemplate }) {
await JobTemplatesAPI.update(template.id, remainingValues);
await Promise.all([
submitLabels(labels, template?.organization),
submitInstanceGroups(instanceGroups, initialInstanceGroups),
submitCredentials(credentials),
JobTemplatesAPI.orderInstanceGroups(
template.id,
instanceGroups,
initialInstanceGroups
),
]);
reloadTemplate();
history.push(detailsUrl);
@ -93,17 +97,6 @@ function JobTemplateEdit({ template, reloadTemplate }) {
return results;
};
const submitInstanceGroups = async (groups, initialGroups) => {
const { added, removed } = getAddedAndRemoved(initialGroups, groups);
const disassociatePromises = await removed.map(group =>
JobTemplatesAPI.disassociateInstanceGroup(template.id, group.id)
);
const associatePromises = await added.map(group =>
JobTemplatesAPI.associateInstanceGroup(template.id, group.id)
);
return Promise.all([...disassociatePromises, ...associatePromises]);
};
const submitCredentials = async newCredentials => {
const { added, removed } = getAddedAndRemoved(
template.summary_fields.credentials,