Fix edit constructed inventory hanging loading state (#14343)

This commit is contained in:
Marliana Lara 2023-08-21 12:36:36 -04:00 committed by GitHub
parent 8c7ab8fcf2
commit 2a1dffd363
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 177 additions and 96 deletions

View File

@ -6,5 +6,20 @@ class ConstructedInventories extends InstanceGroupsMixin(Base) {
super(http);
this.baseUrl = 'api/v2/constructed_inventories/';
}
async readConstructedInventoryOptions(id, method) {
const {
data: { actions },
} = await this.http.options(`${this.baseUrl}${id}/`);
if (actions[method]) {
return actions[method];
}
throw new Error(
`You have insufficient access to this Constructed Inventory.
Please contact your system administrator if there is an issue with your access.`
);
}
}
export default ConstructedInventories;

View File

@ -0,0 +1,51 @@
import ConstructedInventories from './ConstructedInventories';
describe('ConstructedInventoriesAPI', () => {
const constructedInventoryId = 1;
const constructedInventoryMethod = 'PUT';
let ConstructedInventoriesAPI;
let mockHttp;
beforeEach(() => {
const optionsPromise = () =>
Promise.resolve({
data: {
actions: {
PUT: {},
},
},
});
mockHttp = {
options: jest.fn(optionsPromise),
};
ConstructedInventoriesAPI = new ConstructedInventories(mockHttp);
});
afterEach(() => {
jest.resetAllMocks();
});
test('readConstructedInventoryOptions calls options with the expected params', async () => {
await ConstructedInventoriesAPI.readConstructedInventoryOptions(
constructedInventoryId,
constructedInventoryMethod
);
expect(mockHttp.options).toHaveBeenCalledTimes(1);
expect(mockHttp.options).toHaveBeenCalledWith(
`api/v2/constructed_inventories/${constructedInventoryId}/`
);
});
test('readConstructedInventory should throw an error if action method is missing', async () => {
try {
await ConstructedInventoriesAPI.readConstructedInventoryOptions(
constructedInventoryId,
'POST'
);
} catch (error) {
expect(error.message).toContain(
'You have insufficient access to this Constructed Inventory.'
);
}
});
});

View File

@ -1,14 +1,43 @@
import React, { useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Card, PageSection } from '@patternfly/react-core';
import { ConstructedInventoriesAPI, InventoriesAPI } from 'api';
import useRequest from 'hooks/useRequest';
import { CardBody } from 'components/Card';
import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading';
import ConstructedInventoryForm from '../shared/ConstructedInventoryForm';
function ConstructedInventoryAdd() {
const history = useHistory();
const [submitError, setSubmitError] = useState(null);
const {
isLoading: isLoadingOptions,
error: optionsError,
request: fetchOptions,
result: options,
} = useRequest(
useCallback(async () => {
const res = await ConstructedInventoriesAPI.readOptions();
const { data } = res;
return data.actions.POST;
}, []),
null
);
useEffect(() => {
fetchOptions();
}, [fetchOptions]);
if (isLoadingOptions || (!options && !optionsError)) {
return <ContentLoading />;
}
if (optionsError) {
return <ContentError error={optionsError} />;
}
const handleCancel = () => {
history.push('/inventories');
};
@ -48,6 +77,7 @@ function ConstructedInventoryAdd() {
onCancel={handleCancel}
onSubmit={handleSubmit}
submitError={submitError}
options={options}
/>
</CardBody>
</Card>

View File

@ -55,6 +55,7 @@ describe('<ConstructedInventoryAdd />', () => {
context: { router: { history } },
});
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
});
afterEach(() => {

View File

@ -20,6 +20,27 @@ function ConstructedInventoryEdit({ inventory }) {
const detailsUrl = `/inventories/constructed_inventory/${inventory.id}/details`;
const constructedInventoryId = inventory.id;
const {
isLoading: isLoadingOptions,
error: optionsError,
request: fetchOptions,
result: options,
} = useRequest(
useCallback(
() =>
ConstructedInventoriesAPI.readConstructedInventoryOptions(
constructedInventoryId,
'PUT'
),
[constructedInventoryId]
),
null
);
useEffect(() => {
fetchOptions();
}, [fetchOptions]);
const {
result: { initialInstanceGroups, initialInputInventories },
request: fetchedRelatedData,
@ -44,6 +65,7 @@ function ConstructedInventoryEdit({ inventory }) {
isLoading: true,
}
);
useEffect(() => {
fetchedRelatedData();
}, [fetchedRelatedData]);
@ -99,12 +121,12 @@ function ConstructedInventoryEdit({ inventory }) {
const handleCancel = () => history.push(detailsUrl);
if (isLoading) {
return <ContentLoading />;
if (contentError || optionsError) {
return <ContentError error={contentError || optionsError} />;
}
if (contentError) {
return <ContentError error={contentError} />;
if (isLoading || isLoadingOptions || (!options && !optionsError)) {
return <ContentLoading />;
}
return (
@ -116,6 +138,7 @@ function ConstructedInventoryEdit({ inventory }) {
constructedInventory={inventory}
instanceGroups={initialInstanceGroups}
inputInventories={initialInputInventories}
options={options}
/>
</CardBody>
);

View File

@ -51,27 +51,22 @@ describe('<ConstructedInventoryEdit />', () => {
};
beforeEach(async () => {
ConstructedInventoriesAPI.readOptions.mockResolvedValue({
data: {
related: {},
actions: {
POST: {
limit: {
label: 'Limit',
help_text: '',
},
update_cache_timeout: {
label: 'Update cache timeout',
help_text: 'help',
},
verbosity: {
label: 'Verbosity',
help_text: '',
},
},
ConstructedInventoriesAPI.readConstructedInventoryOptions.mockResolvedValue(
{
limit: {
label: 'Limit',
help_text: '',
},
},
});
update_cache_timeout: {
label: 'Update cache timeout',
help_text: 'help',
},
verbosity: {
label: 'Verbosity',
help_text: '',
},
}
);
InventoriesAPI.readInstanceGroups.mockResolvedValue({
data: {
results: associatedInstanceGroups,
@ -169,6 +164,21 @@ describe('<ConstructedInventoryEdit />', () => {
expect(wrapper.find('ContentError').length).toBe(1);
});
test('should throw content error if user has insufficient options permissions', async () => {
expect(wrapper.find('ContentError').length).toBe(0);
ConstructedInventoriesAPI.readConstructedInventoryOptions.mockImplementationOnce(
() => Promise.reject(new Error())
);
await act(async () => {
wrapper = mountWithContexts(
<ConstructedInventoryEdit inventory={mockInv} />
);
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
expect(wrapper.find('ContentError').length).toBe(1);
});
test('unsuccessful form submission should show an error message', async () => {
const error = {
response: {

View File

@ -1,14 +1,10 @@
import React, { useCallback, useEffect } from 'react';
import React, { useCallback } from 'react';
import { Formik, useField, useFormikContext } from 'formik';
import { func, shape } from 'prop-types';
import { t } from '@lingui/macro';
import { ConstructedInventoriesAPI } from 'api';
import { minMaxValue, required } from 'util/validators';
import useRequest from 'hooks/useRequest';
import { Form, FormGroup } from '@patternfly/react-core';
import { VariablesField } from 'components/CodeEditor';
import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading';
import FormActionGroup from 'components/FormActionGroup/FormActionGroup';
import FormField, { FormSubmitError } from 'components/FormField';
import { FormFullWidthLayout, FormColumnLayout } from 'components/FormLayout';
@ -165,6 +161,7 @@ function ConstructedInventoryForm({
onCancel,
onSubmit,
submitError,
options,
}) {
const initialValues = {
kind: 'constructed',
@ -179,32 +176,6 @@ function ConstructedInventoryForm({
source_vars: constructedInventory?.source_vars || '---',
};
const {
isLoading,
error,
request: fetchOptions,
result: options,
} = useRequest(
useCallback(async () => {
const res = await ConstructedInventoriesAPI.readOptions();
const { data } = res;
return data.actions.POST;
}, []),
null
);
useEffect(() => {
fetchOptions();
}, [fetchOptions]);
if (isLoading || (!options && !error)) {
return <ContentLoading />;
}
if (error) {
return <ContentError error={error} />;
}
return (
<Formik initialValues={initialValues} onSubmit={onSubmit}>
{(formik) => (

View File

@ -1,6 +1,5 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { ConstructedInventoriesAPI } from 'api';
import {
mountWithContexts,
waitForElement,
@ -19,38 +18,35 @@ const mockFormValues = {
inputInventories: [{ id: 100, name: 'East' }],
};
const options = {
limit: {
label: 'Limit',
help_text: '',
},
update_cache_timeout: {
label: 'Update cache timeout',
help_text: 'help',
},
verbosity: {
label: 'Verbosity',
help_text: '',
},
};
describe('<ConstructedInventoryForm />', () => {
let wrapper;
const onSubmit = jest.fn();
beforeEach(async () => {
ConstructedInventoriesAPI.readOptions.mockResolvedValue({
data: {
related: {},
actions: {
POST: {
limit: {
label: 'Limit',
help_text: '',
},
update_cache_timeout: {
label: 'Update cache timeout',
help_text: 'help',
},
verbosity: {
label: 'Verbosity',
help_text: '',
},
},
},
},
});
await act(async () => {
wrapper = mountWithContexts(
<ConstructedInventoryForm onCancel={() => {}} onSubmit={onSubmit} />
<ConstructedInventoryForm
onCancel={() => {}}
onSubmit={onSubmit}
options={options}
/>
);
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
});
afterEach(() => {
@ -104,20 +100,4 @@ describe('<ConstructedInventoryForm />', () => {
'The plugin parameter is required.'
);
});
test('should throw content error when option request fails', async () => {
let newWrapper;
ConstructedInventoriesAPI.readOptions.mockImplementationOnce(() =>
Promise.reject(new Error())
);
await act(async () => {
newWrapper = mountWithContexts(
<ConstructedInventoryForm onCancel={() => {}} onSubmit={() => {}} />
);
});
expect(newWrapper.find('ContentError').length).toBe(0);
newWrapper.update();
expect(newWrapper.find('ContentError').length).toBe(1);
jest.clearAllMocks();
});
});