mirror of
https://github.com/ansible/awx.git
synced 2026-01-09 23:12:08 -03:30
Fix edit constructed inventory hanging loading state (#14343)
This commit is contained in:
parent
8c7ab8fcf2
commit
2a1dffd363
@ -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;
|
||||
|
||||
51
awx/ui/src/api/models/ConstructedInventories.test.js
Normal file
51
awx/ui/src/api/models/ConstructedInventories.test.js
Normal 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.'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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>
|
||||
|
||||
@ -55,6 +55,7 @@ describe('<ConstructedInventoryAdd />', () => {
|
||||
context: { router: { history } },
|
||||
});
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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) => (
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user