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
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 => const InstanceGroupsMixin = parent =>
class extends parent { class extends parent {
readInstanceGroups(resourceId, params) { readInstanceGroups(resourceId, params) {
@@ -18,6 +27,19 @@ const InstanceGroupsMixin = parent =>
disassociate: true, 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; export default InstanceGroupsMixin;

View File

@@ -2,7 +2,7 @@ import React, { useCallback, useEffect } from 'react';
import { arrayOf, string, func, bool } from 'prop-types'; import { arrayOf, string, func, bool } from 'prop-types';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { t } from '@lingui/macro'; import { t, Trans } from '@lingui/macro';
import { FormGroup } from '@patternfly/react-core'; import { FormGroup } from '@patternfly/react-core';
import { InstanceGroupsAPI } from '../../api'; import { InstanceGroupsAPI } from '../../api';
import { InstanceGroup } from '../../types'; import { InstanceGroup } from '../../types';
@@ -82,6 +82,18 @@ function InstanceGroupsLookup({
multiple multiple
required={required} required={required}
isLoading={isLoading} 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 }) => ( renderOptionsList={({ state, dispatch, canDelete }) => (
<OptionsList <OptionsList
value={state.selectedItems} value={state.selectedItems}
@@ -113,6 +125,10 @@ function InstanceGroupsLookup({
readOnly={!canDelete} readOnly={!canDelete}
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })} selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
deselectItem={item => dispatch({ type: 'DESELECT_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, organization: organization.id,
...remainingValues, ...remainingValues,
}); });
if (instanceGroups) { /* eslint-disable no-await-in-loop, no-restricted-syntax */
const associatePromises = instanceGroups.map(async ig => for (const group of instanceGroups) {
InventoriesAPI.associateInstanceGroup(inventoryId, ig.id) await InventoriesAPI.associateInstanceGroup(inventoryId, group.id);
);
await Promise.all(associatePromises);
} }
/* eslint-enable no-await-in-loop, no-restricted-syntax */
const url = history.location.pathname.startsWith( const url = history.location.pathname.startsWith(
'/inventories/smart_inventory' '/inventories/smart_inventory'
) )

View File

@@ -6,7 +6,6 @@ import { CardBody } from '../../../components/Card';
import { InventoriesAPI } from '../../../api'; import { InventoriesAPI } from '../../../api';
import ContentLoading from '../../../components/ContentLoading'; import ContentLoading from '../../../components/ContentLoading';
import InventoryForm from '../shared/InventoryForm'; import InventoryForm from '../shared/InventoryForm';
import { getAddedAndRemoved } from '../../../util/lists';
import useIsMounted from '../../../util/useIsMounted'; import useIsMounted from '../../../util/useIsMounted';
function InventoryEdit({ inventory }) { function InventoryEdit({ inventory }) {
@@ -54,20 +53,12 @@ function InventoryEdit({ inventory }) {
organization: organization.id, organization: organization.id,
...remainingValues, ...remainingValues,
}); });
if (instanceGroups) { await InventoriesAPI.orderInstanceGroups(
const { added, removed } = getAddedAndRemoved( inventory.id,
associatedInstanceGroups, instanceGroups,
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 = const url =
history.location.pathname.search('smart') > -1 history.location.pathname.search('smart') > -1
? `/inventories/smart_inventory/${inventory.id}/details` ? `/inventories/smart_inventory/${inventory.id}/details`

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,13 +41,11 @@ function OrganizationAdd() {
...values, ...values,
default_environment: values.default_environment?.id, 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 */ /* eslint-disable no-await-in-loop, no-restricted-syntax */
// Resolve Promises sequentially to maintain order and avoid race condition // 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) { for (const credential of values.galaxy_credentials) {
await OrganizationsAPI.associateGalaxyCredential( await OrganizationsAPI.associateGalaxyCredential(
response.id, response.id,

View File

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

View File

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

View File

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

View File

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

View File

@@ -258,7 +258,14 @@ describe('<OrganizationForm />', () => {
await act(async () => { await act(async () => {
wrapper.find('button[aria-label="Save"]').simulate('click'); 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 () => { test('onSubmit does not get called if max_hosts value is out of range', async () => {
@@ -332,8 +339,8 @@ describe('<OrganizationForm />', () => {
max_hosts: 0, max_hosts: 0,
default_environment: null, default_environment: null,
}, },
[], mockInstanceGroups,
[] mockInstanceGroups
); );
}); });

View File

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

View File

@@ -61,8 +61,12 @@ function JobTemplateEdit({ template, reloadTemplate }) {
await JobTemplatesAPI.update(template.id, remainingValues); await JobTemplatesAPI.update(template.id, remainingValues);
await Promise.all([ await Promise.all([
submitLabels(labels, template?.organization), submitLabels(labels, template?.organization),
submitInstanceGroups(instanceGroups, initialInstanceGroups),
submitCredentials(credentials), submitCredentials(credentials),
JobTemplatesAPI.orderInstanceGroups(
template.id,
instanceGroups,
initialInstanceGroups
),
]); ]);
reloadTemplate(); reloadTemplate();
history.push(detailsUrl); history.push(detailsUrl);
@@ -93,17 +97,6 @@ function JobTemplateEdit({ template, reloadTemplate }) {
return results; 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 submitCredentials = async newCredentials => {
const { added, removed } = getAddedAndRemoved( const { added, removed } = getAddedAndRemoved(
template.summary_fields.credentials, template.summary_fields.credentials,