Merge pull request #8656 from nixocio/ui_issue_8194

Fix Inventory/Project rbac broken on JT form

Reviewed-by: Kersom
             https://github.com/nixocio
This commit is contained in:
softwarefactory-project-zuul[bot] 2021-01-13 18:23:35 +00:00 committed by GitHub
commit 2f16b361f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 187 additions and 8 deletions

View File

@ -57,7 +57,7 @@ function AdHocCommands({ adHocItems, i18n, hasListItems }) {
fetchData();
}, [fetchData]);
const {
isloading: isLaunchLoading,
isLoading: isLaunchLoading,
error: launchError,
request: launchAdHocCommands,
} = useRequest(

View File

@ -16,6 +16,7 @@ const QS_CONFIG = getQSConfig('inventory', {
page: 1,
page_size: 5,
order_by: 'name',
role_level: 'use_role',
});
function InventoryLookup({
@ -29,6 +30,7 @@ function InventoryLookup({
fieldId,
promptId,
promptName,
isOverrideDisabled,
}) {
const {
result: {
@ -57,8 +59,10 @@ function InventoryLookup({
searchableKeys: Object.keys(
actionsResponse.data.actions?.GET || {}
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
canEdit: Boolean(actionsResponse.data.actions.POST),
canEdit:
Boolean(actionsResponse.data.actions.POST) || isOverrideDisabled,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [history.location]),
{
inventories: [],
@ -195,11 +199,13 @@ InventoryLookup.propTypes = {
value: Inventory,
onChange: func.isRequired,
required: bool,
isOverrideDisabled: bool,
};
InventoryLookup.defaultProps = {
value: null,
required: false,
isOverrideDisabled: false,
};
export default withI18n()(withRouter(InventoryLookup));

View File

@ -0,0 +1,87 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import InventoryLookup from './InventoryLookup';
import { InventoriesAPI } from '../../api';
jest.mock('../../api');
const mockedInventories = {
data: {
count: 2,
results: [
{ id: 2, name: 'Bar' },
{ id: 3, name: 'Baz' },
],
},
};
describe('InventoryLookup', () => {
let wrapper;
beforeEach(() => {
InventoriesAPI.read.mockResolvedValue(mockedInventories);
});
afterEach(() => {
jest.clearAllMocks();
wrapper.unmount();
});
test('should render successfully and fetch data', async () => {
InventoriesAPI.readOptions.mockReturnValue({
data: {
actions: {
GET: {},
POST: {},
},
related_search_fields: [],
},
});
await act(async () => {
wrapper = mountWithContexts(<InventoryLookup onChange={() => {}} />);
});
wrapper.update();
expect(InventoriesAPI.read).toHaveBeenCalledTimes(1);
expect(wrapper.find('InventoryLookup')).toHaveLength(1);
expect(wrapper.find('Lookup').prop('isDisabled')).toBe(false);
});
test('inventory lookup should be enabled', async () => {
InventoriesAPI.readOptions.mockReturnValue({
data: {
actions: {
GET: {},
},
related_search_fields: [],
},
});
await act(async () => {
wrapper = mountWithContexts(
<InventoryLookup isOverrideDisabled onChange={() => {}} />
);
});
wrapper.update();
expect(InventoriesAPI.read).toHaveBeenCalledTimes(1);
expect(wrapper.find('InventoryLookup')).toHaveLength(1);
expect(wrapper.find('Lookup').prop('isDisabled')).toBe(false);
});
test('inventory lookup should be disabled', async () => {
InventoriesAPI.readOptions.mockReturnValue({
data: {
actions: {
GET: {},
},
related_search_fields: [],
},
});
await act(async () => {
wrapper = mountWithContexts(<InventoryLookup onChange={() => {}} />);
});
wrapper.update();
expect(InventoriesAPI.read).toHaveBeenCalledTimes(1);
expect(wrapper.find('InventoryLookup')).toHaveLength(1);
expect(wrapper.find('Lookup').prop('isDisabled')).toBe(true);
});
});

View File

@ -18,6 +18,7 @@ const QS_CONFIG = getQSConfig('project', {
page: 1,
page_size: 5,
order_by: 'name',
role_level: 'use_role',
});
function ProjectLookup({
@ -31,6 +32,7 @@ function ProjectLookup({
value,
onBlur,
history,
isOverrideDisabled,
}) {
const autoPopulateLookup = useAutoPopulateLookup(onChange);
const {
@ -57,8 +59,10 @@ function ProjectLookup({
searchableKeys: Object.keys(
actionsResponse.data.actions?.GET || {}
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
canEdit: Boolean(actionsResponse.data.actions.POST),
canEdit:
Boolean(actionsResponse.data.actions.POST) || isOverrideDisabled,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoPopulate, autoPopulateLookup, history.location.search]),
{
count: 0,
@ -160,6 +164,7 @@ ProjectLookup.propTypes = {
required: bool,
tooltip: string,
value: Project,
isOverrideDisabled: bool,
};
ProjectLookup.defaultProps = {
@ -170,6 +175,7 @@ ProjectLookup.defaultProps = {
required: false,
tooltip: '',
value: null,
isOverrideDisabled: false,
};
export { ProjectLookup as _ProjectLookup };

View File

@ -7,6 +7,10 @@ import ProjectLookup from './ProjectLookup';
jest.mock('../../api');
describe('<ProjectLookup />', () => {
afterEach(() => {
jest.clearAllMocks();
});
test('should auto-select project when only one available and autoPopulate prop is true', async () => {
ProjectsAPI.read.mockReturnValue({
data: {
@ -48,4 +52,46 @@ describe('<ProjectLookup />', () => {
});
expect(onChange).not.toHaveBeenCalled();
});
test('project lookup should be enabled', async () => {
let wrapper;
ProjectsAPI.readOptions.mockReturnValue({
data: {
actions: {
GET: {},
},
related_search_fields: [],
},
});
await act(async () => {
wrapper = mountWithContexts(
<ProjectLookup isOverrideDisabled onChange={() => {}} />
);
});
wrapper.update();
expect(ProjectsAPI.read).toHaveBeenCalledTimes(1);
expect(wrapper.find('ProjectLookup')).toHaveLength(1);
expect(wrapper.find('Lookup').prop('isDisabled')).toBe(false);
});
test('project lookup should be disabled', async () => {
let wrapper;
ProjectsAPI.readOptions.mockReturnValue({
data: {
actions: {
GET: {},
},
related_search_fields: [],
},
});
await act(async () => {
wrapper = mountWithContexts(<ProjectLookup onChange={() => {}} />);
});
wrapper.update();
expect(ProjectsAPI.read).toHaveBeenCalledTimes(1);
expect(wrapper.find('ProjectLookup')).toHaveLength(1);
expect(wrapper.find('Lookup').prop('isDisabled')).toBe(true);
});
});

View File

@ -83,6 +83,7 @@ function JobTemplateAdd() {
handleCancel={handleCancel}
handleSubmit={handleSubmit}
submitError={formSubmitError}
isOverrideDisabledLookup
/>
</CardBody>
</Card>

View File

@ -1,20 +1,45 @@
/* eslint react/no-unused-state: 0 */
import React, { useState } from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import { Redirect, useHistory } from 'react-router-dom';
import { CardBody } from '../../../components/Card';
import { JobTemplatesAPI } from '../../../api';
import { JobTemplate } from '../../../types';
import { JobTemplatesAPI, ProjectsAPI } from '../../../api';
import { getAddedAndRemoved } from '../../../util/lists';
import useRequest from '../../../util/useRequest';
import JobTemplateForm from '../shared/JobTemplateForm';
import ContentLoading from '../../../components/ContentLoading';
import { CardBody } from '../../../components/Card';
function JobTemplateEdit({ template }) {
const history = useHistory();
const [formSubmitError, setFormSubmitError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [isDisabled, setIsDisabled] = useState(false);
const detailsUrl = `/templates/${template.type}/${template.id}/details`;
const {
request: fetchProject,
error: fetchProjectError,
isLoading: projectLoading,
} = useRequest(
useCallback(async () => {
await ProjectsAPI.readDetail(template.project);
}, [template.project])
);
useEffect(() => {
fetchProject();
}, [fetchProject]);
useEffect(() => {
if (fetchProjectError) {
if (fetchProjectError.response.status === 403) {
setIsDisabled(true);
}
}
}, [fetchProjectError]);
const handleSubmit = async values => {
const {
labels,
@ -89,7 +114,7 @@ function JobTemplateEdit({ template }) {
const associateCredentials = added.map(cred =>
JobTemplatesAPI.associateCredentials(template.id, cred.id)
);
const associatePromise = Promise.all(associateCredentials);
const associatePromise = await Promise.all(associateCredentials);
return Promise.all([disassociatePromise, associatePromise]);
};
@ -100,9 +125,10 @@ function JobTemplateEdit({ template }) {
if (!canEdit) {
return <Redirect to={detailsUrl} />;
}
if (isLoading) {
if (isLoading || projectLoading) {
return <ContentLoading />;
}
return (
<CardBody>
<JobTemplateForm
@ -110,6 +136,7 @@ function JobTemplateEdit({ template }) {
handleCancel={handleCancel}
handleSubmit={handleSubmit}
submitError={formSubmitError}
isOverrideDisabledLookup={!isDisabled}
/>
</CardBody>
);

View File

@ -53,6 +53,7 @@ function JobTemplateForm({
setFieldValue,
submitError,
i18n,
isOverrideDisabledLookup,
}) {
const [contentError, setContentError] = useState(false);
const [inventory, setInventory] = useState(
@ -254,6 +255,7 @@ function JobTemplateForm({
required={!askInventoryOnLaunchField.value}
touched={inventoryMeta.touched}
error={inventoryMeta.error}
isOverrideDisabled={isOverrideDisabledLookup}
/>
</FormGroup>
<ProjectLookup
@ -266,6 +268,7 @@ function JobTemplateForm({
onChange={handleProjectUpdate}
required
autoPopulate={!template?.id}
isOverrideDisabled={isOverrideDisabledLookup}
/>
{projectField.value?.allow_override && (
<FieldWithPrompt
@ -623,7 +626,9 @@ JobTemplateForm.propTypes = {
handleCancel: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
submitError: PropTypes.shape({}),
isOverrideDisabledLookup: PropTypes.bool,
};
JobTemplateForm.defaultProps = {
template: {
name: '',
@ -641,6 +646,7 @@ JobTemplateForm.defaultProps = {
isNew: true,
},
submitError: null,
isOverrideDisabledLookup: false,
};
const FormikApp = withFormik({