mirror of
https://github.com/ansible/awx.git
synced 2026-03-11 06:29:31 -02:30
Fix Inventory/Project rbac broken on JT form
Fix Inventory/Project rbac broken on JT form. Also, update ProjectLookup to filter using `role_level: 'use_role'` as per old UI implementation. Also, update InventoryLookup to filter using `role_level: 'use_role'` as per old UI implementation. See: https://github.com/ansible/awx/issues/8194
This commit is contained in:
@@ -57,7 +57,7 @@ function AdHocCommands({ adHocItems, i18n, hasListItems }) {
|
|||||||
fetchData();
|
fetchData();
|
||||||
}, [fetchData]);
|
}, [fetchData]);
|
||||||
const {
|
const {
|
||||||
isloading: isLaunchLoading,
|
isLoading: isLaunchLoading,
|
||||||
error: launchError,
|
error: launchError,
|
||||||
request: launchAdHocCommands,
|
request: launchAdHocCommands,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const QS_CONFIG = getQSConfig('inventory', {
|
|||||||
page: 1,
|
page: 1,
|
||||||
page_size: 5,
|
page_size: 5,
|
||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
|
role_level: 'use_role',
|
||||||
});
|
});
|
||||||
|
|
||||||
function InventoryLookup({
|
function InventoryLookup({
|
||||||
@@ -29,6 +30,7 @@ function InventoryLookup({
|
|||||||
fieldId,
|
fieldId,
|
||||||
promptId,
|
promptId,
|
||||||
promptName,
|
promptName,
|
||||||
|
isOverrideDisabled,
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
result: {
|
result: {
|
||||||
@@ -57,8 +59,10 @@ function InventoryLookup({
|
|||||||
searchableKeys: Object.keys(
|
searchableKeys: Object.keys(
|
||||||
actionsResponse.data.actions?.GET || {}
|
actionsResponse.data.actions?.GET || {}
|
||||||
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
).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]),
|
}, [history.location]),
|
||||||
{
|
{
|
||||||
inventories: [],
|
inventories: [],
|
||||||
@@ -195,11 +199,13 @@ InventoryLookup.propTypes = {
|
|||||||
value: Inventory,
|
value: Inventory,
|
||||||
onChange: func.isRequired,
|
onChange: func.isRequired,
|
||||||
required: bool,
|
required: bool,
|
||||||
|
isOverrideDisabled: bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
InventoryLookup.defaultProps = {
|
InventoryLookup.defaultProps = {
|
||||||
value: null,
|
value: null,
|
||||||
required: false,
|
required: false,
|
||||||
|
isOverrideDisabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(withRouter(InventoryLookup));
|
export default withI18n()(withRouter(InventoryLookup));
|
||||||
|
|||||||
87
awx/ui_next/src/components/Lookup/InventoryLookup.test.jsx
Normal file
87
awx/ui_next/src/components/Lookup/InventoryLookup.test.jsx
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -18,6 +18,7 @@ const QS_CONFIG = getQSConfig('project', {
|
|||||||
page: 1,
|
page: 1,
|
||||||
page_size: 5,
|
page_size: 5,
|
||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
|
role_level: 'use_role',
|
||||||
});
|
});
|
||||||
|
|
||||||
function ProjectLookup({
|
function ProjectLookup({
|
||||||
@@ -31,6 +32,7 @@ function ProjectLookup({
|
|||||||
value,
|
value,
|
||||||
onBlur,
|
onBlur,
|
||||||
history,
|
history,
|
||||||
|
isOverrideDisabled,
|
||||||
}) {
|
}) {
|
||||||
const autoPopulateLookup = useAutoPopulateLookup(onChange);
|
const autoPopulateLookup = useAutoPopulateLookup(onChange);
|
||||||
const {
|
const {
|
||||||
@@ -57,8 +59,10 @@ function ProjectLookup({
|
|||||||
searchableKeys: Object.keys(
|
searchableKeys: Object.keys(
|
||||||
actionsResponse.data.actions?.GET || {}
|
actionsResponse.data.actions?.GET || {}
|
||||||
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
).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]),
|
}, [autoPopulate, autoPopulateLookup, history.location.search]),
|
||||||
{
|
{
|
||||||
count: 0,
|
count: 0,
|
||||||
@@ -160,6 +164,7 @@ ProjectLookup.propTypes = {
|
|||||||
required: bool,
|
required: bool,
|
||||||
tooltip: string,
|
tooltip: string,
|
||||||
value: Project,
|
value: Project,
|
||||||
|
isOverrideDisabled: bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
ProjectLookup.defaultProps = {
|
ProjectLookup.defaultProps = {
|
||||||
@@ -170,6 +175,7 @@ ProjectLookup.defaultProps = {
|
|||||||
required: false,
|
required: false,
|
||||||
tooltip: '',
|
tooltip: '',
|
||||||
value: null,
|
value: null,
|
||||||
|
isOverrideDisabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { ProjectLookup as _ProjectLookup };
|
export { ProjectLookup as _ProjectLookup };
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import ProjectLookup from './ProjectLookup';
|
|||||||
jest.mock('../../api');
|
jest.mock('../../api');
|
||||||
|
|
||||||
describe('<ProjectLookup />', () => {
|
describe('<ProjectLookup />', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
test('should auto-select project when only one available and autoPopulate prop is true', async () => {
|
test('should auto-select project when only one available and autoPopulate prop is true', async () => {
|
||||||
ProjectsAPI.read.mockReturnValue({
|
ProjectsAPI.read.mockReturnValue({
|
||||||
data: {
|
data: {
|
||||||
@@ -48,4 +52,46 @@ describe('<ProjectLookup />', () => {
|
|||||||
});
|
});
|
||||||
expect(onChange).not.toHaveBeenCalled();
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ function JobTemplateAdd() {
|
|||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
submitError={formSubmitError}
|
submitError={formSubmitError}
|
||||||
|
isOverrideDisabledLookup
|
||||||
/>
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,20 +1,45 @@
|
|||||||
/* eslint react/no-unused-state: 0 */
|
/* 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 { Redirect, useHistory } from 'react-router-dom';
|
||||||
import { CardBody } from '../../../components/Card';
|
|
||||||
import { JobTemplatesAPI } from '../../../api';
|
|
||||||
import { JobTemplate } from '../../../types';
|
import { JobTemplate } from '../../../types';
|
||||||
|
import { JobTemplatesAPI, ProjectsAPI } from '../../../api';
|
||||||
import { getAddedAndRemoved } from '../../../util/lists';
|
import { getAddedAndRemoved } from '../../../util/lists';
|
||||||
|
import useRequest from '../../../util/useRequest';
|
||||||
import JobTemplateForm from '../shared/JobTemplateForm';
|
import JobTemplateForm from '../shared/JobTemplateForm';
|
||||||
import ContentLoading from '../../../components/ContentLoading';
|
import ContentLoading from '../../../components/ContentLoading';
|
||||||
|
import { CardBody } from '../../../components/Card';
|
||||||
|
|
||||||
function JobTemplateEdit({ template }) {
|
function JobTemplateEdit({ template }) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [formSubmitError, setFormSubmitError] = useState(null);
|
const [formSubmitError, setFormSubmitError] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isDisabled, setIsDisabled] = useState(false);
|
||||||
|
|
||||||
const detailsUrl = `/templates/${template.type}/${template.id}/details`;
|
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 handleSubmit = async values => {
|
||||||
const {
|
const {
|
||||||
labels,
|
labels,
|
||||||
@@ -89,7 +114,7 @@ function JobTemplateEdit({ template }) {
|
|||||||
const associateCredentials = added.map(cred =>
|
const associateCredentials = added.map(cred =>
|
||||||
JobTemplatesAPI.associateCredentials(template.id, cred.id)
|
JobTemplatesAPI.associateCredentials(template.id, cred.id)
|
||||||
);
|
);
|
||||||
const associatePromise = Promise.all(associateCredentials);
|
const associatePromise = await Promise.all(associateCredentials);
|
||||||
return Promise.all([disassociatePromise, associatePromise]);
|
return Promise.all([disassociatePromise, associatePromise]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,9 +125,10 @@ function JobTemplateEdit({ template }) {
|
|||||||
if (!canEdit) {
|
if (!canEdit) {
|
||||||
return <Redirect to={detailsUrl} />;
|
return <Redirect to={detailsUrl} />;
|
||||||
}
|
}
|
||||||
if (isLoading) {
|
if (isLoading || projectLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<JobTemplateForm
|
<JobTemplateForm
|
||||||
@@ -110,6 +136,7 @@ function JobTemplateEdit({ template }) {
|
|||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
submitError={formSubmitError}
|
submitError={formSubmitError}
|
||||||
|
isOverrideDisabledLookup={!isDisabled}
|
||||||
/>
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ function JobTemplateForm({
|
|||||||
setFieldValue,
|
setFieldValue,
|
||||||
submitError,
|
submitError,
|
||||||
i18n,
|
i18n,
|
||||||
|
isOverrideDisabledLookup,
|
||||||
}) {
|
}) {
|
||||||
const [contentError, setContentError] = useState(false);
|
const [contentError, setContentError] = useState(false);
|
||||||
const [inventory, setInventory] = useState(
|
const [inventory, setInventory] = useState(
|
||||||
@@ -254,6 +255,7 @@ function JobTemplateForm({
|
|||||||
required={!askInventoryOnLaunchField.value}
|
required={!askInventoryOnLaunchField.value}
|
||||||
touched={inventoryMeta.touched}
|
touched={inventoryMeta.touched}
|
||||||
error={inventoryMeta.error}
|
error={inventoryMeta.error}
|
||||||
|
isOverrideDisabled={isOverrideDisabledLookup}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<ProjectLookup
|
<ProjectLookup
|
||||||
@@ -266,6 +268,7 @@ function JobTemplateForm({
|
|||||||
onChange={handleProjectUpdate}
|
onChange={handleProjectUpdate}
|
||||||
required
|
required
|
||||||
autoPopulate={!template?.id}
|
autoPopulate={!template?.id}
|
||||||
|
isOverrideDisabled={isOverrideDisabledLookup}
|
||||||
/>
|
/>
|
||||||
{projectField.value?.allow_override && (
|
{projectField.value?.allow_override && (
|
||||||
<FieldWithPrompt
|
<FieldWithPrompt
|
||||||
@@ -623,7 +626,9 @@ JobTemplateForm.propTypes = {
|
|||||||
handleCancel: PropTypes.func.isRequired,
|
handleCancel: PropTypes.func.isRequired,
|
||||||
handleSubmit: PropTypes.func.isRequired,
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
submitError: PropTypes.shape({}),
|
submitError: PropTypes.shape({}),
|
||||||
|
isOverrideDisabledLookup: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
JobTemplateForm.defaultProps = {
|
JobTemplateForm.defaultProps = {
|
||||||
template: {
|
template: {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -641,6 +646,7 @@ JobTemplateForm.defaultProps = {
|
|||||||
isNew: true,
|
isNew: true,
|
||||||
},
|
},
|
||||||
submitError: null,
|
submitError: null,
|
||||||
|
isOverrideDisabledLookup: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormikApp = withFormik({
|
const FormikApp = withFormik({
|
||||||
|
|||||||
Reference in New Issue
Block a user