Skip to content

Commit

Permalink
Fix edit constructed inventory hanging loading state (#14343)
Browse files Browse the repository at this point in the history
  • Loading branch information
marshmalien authored Aug 21, 2023
1 parent 8c7ab8f commit 2a1dffd
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 96 deletions.
15 changes: 15 additions & 0 deletions awx/ui/src/api/models/ConstructedInventories.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 51 additions & 0 deletions awx/ui/src/api/models/ConstructedInventories.test.js
Original file line number Diff line number Diff line change
@@ -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.'
);
}
});
});
Original file line number Diff line number Diff line change
@@ -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');
};
Expand Down Expand Up @@ -48,6 +77,7 @@ function ConstructedInventoryAdd() {
onCancel={handleCancel}
onSubmit={handleSubmit}
submitError={submitError}
options={options}
/>
</CardBody>
</Card>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe('<ConstructedInventoryAdd />', () => {
context: { router: { history } },
});
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
});

afterEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -44,6 +65,7 @@ function ConstructedInventoryEdit({ inventory }) {
isLoading: true,
}
);

useEffect(() => {
fetchedRelatedData();
}, [fetchedRelatedData]);
Expand Down Expand Up @@ -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 (
Expand All @@ -116,6 +138,7 @@ function ConstructedInventoryEdit({ inventory }) {
constructedInventory={inventory}
instanceGroups={initialInstanceGroups}
inputInventories={initialInputInventories}
options={options}
/>
</CardBody>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: {
Expand Down
33 changes: 2 additions & 31 deletions awx/ui/src/screens/Inventory/shared/ConstructedInventoryForm.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -165,6 +161,7 @@ function ConstructedInventoryForm({
onCancel,
onSubmit,
submitError,
options,
}) {
const initialValues = {
kind: 'constructed',
Expand All @@ -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) => (
Expand Down
Loading

0 comments on commit 2a1dffd

Please sign in to comment.