mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 18:09:57 -03:30
Merge pull request #7103 from AlexSCorey/6921-UserAndTeamsAccessAdd
Adds support to user and team access add
Reviewed-by: Alex Corey <Alex.swansboro@gmail.com>
https://github.com/AlexSCorey
This commit is contained in:
commit
d205685541
@ -258,7 +258,7 @@ class AddResourceRole extends React.Component {
|
||||
sortColumns={userSortColumns}
|
||||
displayKey="username"
|
||||
onRowClick={this.handleResourceCheckboxClick}
|
||||
onSearch={readUsers}
|
||||
fetchItems={readUsers}
|
||||
selectedLabel={i18n._(t`Selected`)}
|
||||
selectedResourceRows={selectedResourceRows}
|
||||
sortedColumnKey="username"
|
||||
@ -269,7 +269,7 @@ class AddResourceRole extends React.Component {
|
||||
searchColumns={teamSearchColumns}
|
||||
sortColumns={teamSortColumns}
|
||||
onRowClick={this.handleResourceCheckboxClick}
|
||||
onSearch={readTeams}
|
||||
fetchItems={readTeams}
|
||||
selectedLabel={i18n._(t`Selected`)}
|
||||
selectedResourceRows={selectedResourceRows}
|
||||
/>
|
||||
|
||||
@ -1,19 +1,27 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Checkbox } from '@patternfly/react-core';
|
||||
import { Checkbox as PFCheckbox } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const CheckboxWrapper = styled.div`
|
||||
display: flex;
|
||||
border: 1px solid var(--pf-global--BorderColor--200);
|
||||
border-radius: var(--pf-global--BorderRadius--sm);
|
||||
padding: 10px;
|
||||
`;
|
||||
|
||||
const Checkbox = styled(PFCheckbox)`
|
||||
width: 100%;
|
||||
& label {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
class CheckboxCard extends Component {
|
||||
render() {
|
||||
const { name, description, isSelected, onSelect, itemId } = this.props;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
border: '1px solid var(--pf-global--BorderColor--200)',
|
||||
borderRadius: 'var(--pf-global--BorderRadius--sm)',
|
||||
padding: '10px',
|
||||
}}
|
||||
>
|
||||
<CheckboxWrapper>
|
||||
<Checkbox
|
||||
isChecked={isSelected}
|
||||
onChange={onSelect}
|
||||
@ -27,7 +35,7 @@ class CheckboxCard extends Component {
|
||||
}
|
||||
value={itemId}
|
||||
/>
|
||||
</div>
|
||||
</CheckboxWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import React, { Fragment, useCallback, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withRouter, useLocation } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import useRequest from '../../util/useRequest';
|
||||
|
||||
import { SearchColumns, SortColumns } from '../../types';
|
||||
import PaginatedDataList from '../PaginatedDataList';
|
||||
import DataListToolbar from '../DataListToolbar';
|
||||
@ -10,124 +12,94 @@ import CheckboxListItem from '../CheckboxListItem';
|
||||
import SelectedList from '../SelectedList';
|
||||
import { getQSConfig, parseQueryString } from '../../util/qs';
|
||||
|
||||
class SelectResourceStep extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const QS_Config = sortColumns => {
|
||||
return getQSConfig('resource', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: `${
|
||||
sortColumns.filter(col => col.key === 'name').length ? 'name' : 'username'
|
||||
}`,
|
||||
});
|
||||
};
|
||||
function SelectResourceStep({
|
||||
searchColumns,
|
||||
sortColumns,
|
||||
displayKey,
|
||||
onRowClick,
|
||||
selectedLabel,
|
||||
selectedResourceRows,
|
||||
fetchItems,
|
||||
i18n,
|
||||
}) {
|
||||
const location = useLocation();
|
||||
|
||||
this.state = {
|
||||
isInitialized: false,
|
||||
count: null,
|
||||
error: false,
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
request: readResourceList,
|
||||
result: { resources, itemCount },
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const queryParams = parseQueryString(
|
||||
QS_Config(sortColumns),
|
||||
location.search
|
||||
);
|
||||
|
||||
const {
|
||||
data: { count, results },
|
||||
} = await fetchItems(queryParams);
|
||||
return { resources: results, itemCount: count };
|
||||
}, [location, fetchItems, sortColumns]),
|
||||
{
|
||||
resources: [],
|
||||
};
|
||||
|
||||
this.qsConfig = getQSConfig('resource', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: `${
|
||||
props.sortColumns.filter(col => col.key === 'name').length
|
||||
? 'name'
|
||||
: 'username'
|
||||
}`,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.readResourceList();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { location } = this.props;
|
||||
if (location !== prevProps.location) {
|
||||
this.readResourceList();
|
||||
itemCount: 0,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async readResourceList() {
|
||||
const { onSearch, location } = this.props;
|
||||
const queryParams = parseQueryString(this.qsConfig, location.search);
|
||||
useEffect(() => {
|
||||
readResourceList();
|
||||
}, [readResourceList]);
|
||||
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
error: false,
|
||||
});
|
||||
try {
|
||||
const { data } = await onSearch(queryParams);
|
||||
const { count, results } = data;
|
||||
|
||||
this.setState({
|
||||
resources: results,
|
||||
count,
|
||||
isInitialized: true,
|
||||
isLoading: false,
|
||||
error: false,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isInitialized, isLoading, count, error, resources } = this.state;
|
||||
|
||||
const {
|
||||
searchColumns,
|
||||
sortColumns,
|
||||
displayKey,
|
||||
onRowClick,
|
||||
selectedLabel,
|
||||
selectedResourceRows,
|
||||
i18n,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{isInitialized && (
|
||||
<Fragment>
|
||||
<div>
|
||||
{i18n._(
|
||||
t`Choose the resources that will be receiving new roles. You'll be able to select the roles to apply in the next step. Note that the resources chosen here will receive all roles chosen in the next step.`
|
||||
)}
|
||||
</div>
|
||||
{selectedResourceRows.length > 0 && (
|
||||
<SelectedList
|
||||
displayKey={displayKey}
|
||||
label={selectedLabel}
|
||||
onRemove={onRowClick}
|
||||
selected={selectedResourceRows}
|
||||
/>
|
||||
)}
|
||||
<PaginatedDataList
|
||||
hasContentLoading={isLoading}
|
||||
items={resources}
|
||||
itemCount={count}
|
||||
qsConfig={this.qsConfig}
|
||||
onRowClick={onRowClick}
|
||||
toolbarSearchColumns={searchColumns}
|
||||
toolbarSortColumns={sortColumns}
|
||||
renderItem={item => (
|
||||
<CheckboxListItem
|
||||
isSelected={selectedResourceRows.some(i => i.id === item.id)}
|
||||
itemId={item.id}
|
||||
key={item.id}
|
||||
name={item[displayKey]}
|
||||
label={item[displayKey]}
|
||||
onSelect={() => onRowClick(item)}
|
||||
onDeselect={() => onRowClick(item)}
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
||||
showPageSizeOptions={false}
|
||||
/>
|
||||
</Fragment>
|
||||
return (
|
||||
<Fragment>
|
||||
<div>
|
||||
{i18n._(
|
||||
t`Choose the resources that will be receiving new roles. You'll be able to select the roles to apply in the next step. Note that the resources chosen here will receive all roles chosen in the next step.`
|
||||
)}
|
||||
{error ? <div>error</div> : ''}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
{selectedResourceRows.length > 0 && (
|
||||
<SelectedList
|
||||
displayKey={displayKey}
|
||||
label={selectedLabel}
|
||||
onRemove={onRowClick}
|
||||
selected={selectedResourceRows}
|
||||
/>
|
||||
)}
|
||||
<PaginatedDataList
|
||||
hasContentLoading={isLoading}
|
||||
contentError={error}
|
||||
items={resources}
|
||||
itemCount={itemCount}
|
||||
qsConfig={QS_Config(sortColumns)}
|
||||
onRowClick={onRowClick}
|
||||
toolbarSearchColumns={searchColumns}
|
||||
toolbarSortColumns={sortColumns}
|
||||
renderItem={item => (
|
||||
<CheckboxListItem
|
||||
isSelected={selectedResourceRows.some(i => i.id === item.id)}
|
||||
itemId={item.id}
|
||||
key={item.id}
|
||||
name={item[displayKey]}
|
||||
label={item[displayKey]}
|
||||
onSelect={() => onRowClick(item)}
|
||||
onDeselect={() => onRowClick(item)}
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
||||
showPageSizeOptions={false}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
SelectResourceStep.propTypes = {
|
||||
@ -135,7 +107,7 @@ SelectResourceStep.propTypes = {
|
||||
sortColumns: SortColumns,
|
||||
displayKey: PropTypes.string,
|
||||
onRowClick: PropTypes.func,
|
||||
onSearch: PropTypes.func.isRequired,
|
||||
fetchItems: PropTypes.func.isRequired,
|
||||
selectedLabel: PropTypes.string,
|
||||
selectedResourceRows: PropTypes.arrayOf(PropTypes.object),
|
||||
};
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import React from 'react';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
import { sleep } from '../../../testUtils/testUtils';
|
||||
import SelectResourceStep from './SelectResourceStep';
|
||||
|
||||
@ -30,12 +34,12 @@ describe('<SelectResourceStep />', () => {
|
||||
sortColumns={sortColumns}
|
||||
displayKey="username"
|
||||
onRowClick={() => {}}
|
||||
onSearch={() => {}}
|
||||
fetchItems={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
test('fetches resources on mount', async () => {
|
||||
test('fetches resources on mount and adds items to list', async () => {
|
||||
const handleSearch = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
count: 2,
|
||||
@ -45,61 +49,24 @@ describe('<SelectResourceStep />', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
mountWithContexts(
|
||||
<SelectResourceStep
|
||||
searchColumns={searchColumns}
|
||||
sortColumns={sortColumns}
|
||||
displayKey="username"
|
||||
onRowClick={() => {}}
|
||||
onSearch={handleSearch}
|
||||
/>
|
||||
);
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SelectResourceStep
|
||||
searchColumns={searchColumns}
|
||||
sortColumns={sortColumns}
|
||||
displayKey="username"
|
||||
onRowClick={() => {}}
|
||||
fetchItems={handleSearch}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(handleSearch).toHaveBeenCalledWith({
|
||||
order_by: 'username',
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
});
|
||||
});
|
||||
|
||||
test('readResourceList properly adds rows to state', async () => {
|
||||
const selectedResourceRows = [{ id: 1, username: 'foo', url: 'item/1' }];
|
||||
const handleSearch = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
count: 2,
|
||||
results: [
|
||||
{ id: 1, username: 'foo', url: 'item/1' },
|
||||
{ id: 2, username: 'bar', url: 'item/2' },
|
||||
],
|
||||
},
|
||||
});
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: [
|
||||
'/organizations/1/access?resource.page=1&resource.order_by=-username',
|
||||
],
|
||||
});
|
||||
const wrapper = mountWithContexts(
|
||||
<SelectResourceStep
|
||||
searchColumns={searchColumns}
|
||||
sortColumns={sortColumns}
|
||||
displayKey="username"
|
||||
onRowClick={() => {}}
|
||||
onSearch={handleSearch}
|
||||
selectedResourceRows={selectedResourceRows}
|
||||
/>,
|
||||
{
|
||||
context: { router: { history, route: { location: history.location } } },
|
||||
}
|
||||
).find('SelectResourceStep');
|
||||
await wrapper.instance().readResourceList();
|
||||
expect(handleSearch).toHaveBeenCalledWith({
|
||||
order_by: '-username',
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
});
|
||||
expect(wrapper.state('resources')).toEqual([
|
||||
{ id: 1, username: 'foo', url: 'item/1' },
|
||||
{ id: 2, username: 'bar', url: 'item/2' },
|
||||
]);
|
||||
waitForElement(wrapper, 'CheckBoxListItem', el => el.length === 2);
|
||||
});
|
||||
|
||||
test('clicking on row fires callback with correct params', async () => {
|
||||
@ -111,20 +78,24 @@ describe('<SelectResourceStep />', () => {
|
||||
{ id: 2, username: 'bar', url: 'item/2' },
|
||||
],
|
||||
};
|
||||
const wrapper = mountWithContexts(
|
||||
<SelectResourceStep
|
||||
searchColumns={searchColumns}
|
||||
sortColumns={sortColumns}
|
||||
displayKey="username"
|
||||
onRowClick={handleRowClick}
|
||||
onSearch={() => ({ data })}
|
||||
selectedResourceRows={[]}
|
||||
/>
|
||||
);
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SelectResourceStep
|
||||
searchColumns={searchColumns}
|
||||
sortColumns={sortColumns}
|
||||
displayKey="username"
|
||||
onRowClick={handleRowClick}
|
||||
fetchItems={() => ({ data })}
|
||||
selectedResourceRows={[]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await sleep(0);
|
||||
wrapper.update();
|
||||
const checkboxListItemWrapper = wrapper.find('CheckboxListItem');
|
||||
expect(checkboxListItemWrapper.length).toBe(2);
|
||||
|
||||
checkboxListItemWrapper
|
||||
.first()
|
||||
.find('input[type="checkbox"]')
|
||||
|
||||
@ -0,0 +1,156 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import useRequest, { useDismissableError } from '../../util/useRequest';
|
||||
import SelectableCard from '../SelectableCard';
|
||||
import AlertModal from '../AlertModal';
|
||||
import ErrorDetail from '../ErrorDetail';
|
||||
import Wizard from '../Wizard/Wizard';
|
||||
import useSelected from '../../util/useSelected';
|
||||
import SelectResourceStep from '../AddRole/SelectResourceStep';
|
||||
import SelectRoleStep from '../AddRole/SelectRoleStep';
|
||||
import getResourceAccessConfig from './getResourceAccessConfig';
|
||||
|
||||
const Grid = styled.div`
|
||||
display: grid;
|
||||
grid-gap: 20px;
|
||||
grid-template-columns: 33% 33% 33%;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
`;
|
||||
|
||||
function UserAndTeamAccessAdd({
|
||||
i18n,
|
||||
isOpen,
|
||||
title,
|
||||
onSave,
|
||||
apiModel,
|
||||
onClose,
|
||||
}) {
|
||||
const [selectedResourceType, setSelectedResourceType] = useState(null);
|
||||
const [stepIdReached, setStepIdReached] = useState(1);
|
||||
const { id: userId } = useParams();
|
||||
const {
|
||||
selected: resourcesSelected,
|
||||
handleSelect: handleResourceSelect,
|
||||
} = useSelected([]);
|
||||
|
||||
const {
|
||||
selected: rolesSelected,
|
||||
handleSelect: handleRoleSelect,
|
||||
} = useSelected([]);
|
||||
|
||||
const { request: handleWizardSave, error: saveError } = useRequest(
|
||||
useCallback(async () => {
|
||||
const roleRequests = [];
|
||||
const resourceRolesTypes = resourcesSelected.flatMap(resource =>
|
||||
Object.values(resource.summary_fields.object_roles)
|
||||
);
|
||||
|
||||
rolesSelected.map(role =>
|
||||
resourceRolesTypes.forEach(rolename => {
|
||||
if (rolename.name === role.name) {
|
||||
roleRequests.push(apiModel.associateRole(userId, rolename.id));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(roleRequests);
|
||||
onSave();
|
||||
}, [onSave, rolesSelected, apiModel, userId, resourcesSelected]),
|
||||
{}
|
||||
);
|
||||
|
||||
const { error, dismissError } = useDismissableError(saveError);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: 1,
|
||||
name: i18n._(t`Add resource type`),
|
||||
component: (
|
||||
<Grid>
|
||||
{getResourceAccessConfig(i18n).map(resource => (
|
||||
<SelectableCard
|
||||
key={resource.selectedResource}
|
||||
isSelected={
|
||||
resource.selectedResource ===
|
||||
selectedResourceType?.selectedResource
|
||||
}
|
||||
label={resource.label}
|
||||
dataCy={`add-role-${resource.selectedResource}`}
|
||||
onClick={() => setSelectedResourceType(resource)}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
),
|
||||
enableNext: selectedResourceType !== null,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: i18n._(t`Select items from list`),
|
||||
component: selectedResourceType && (
|
||||
<SelectResourceStep
|
||||
searchColumns={selectedResourceType.searchColumns}
|
||||
sortColumns={selectedResourceType.sortColumns}
|
||||
displayKey="name"
|
||||
onRowClick={handleResourceSelect}
|
||||
fetchItems={selectedResourceType.fetchItems}
|
||||
selectedLabel={i18n._(t`Selected`)}
|
||||
selectedResourceRows={resourcesSelected}
|
||||
sortedColumnKey="username"
|
||||
/>
|
||||
),
|
||||
enableNext: resourcesSelected.length > 0,
|
||||
canJumpTo: stepIdReached >= 2,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: i18n._(t`Select roles to apply`),
|
||||
component: resourcesSelected?.length > 0 && (
|
||||
<SelectRoleStep
|
||||
onRolesClick={handleRoleSelect}
|
||||
roles={resourcesSelected[0].summary_fields.object_roles}
|
||||
selectedListKey={
|
||||
selectedResourceType === 'users' ? 'username' : 'name'
|
||||
}
|
||||
selectedListLabel={i18n._(t`Selected`)}
|
||||
selectedResourceRows={resourcesSelected}
|
||||
selectedRoleRows={rolesSelected}
|
||||
/>
|
||||
),
|
||||
nextButtonText: i18n._(t`Save`),
|
||||
canJumpTo: stepIdReached >= 3,
|
||||
},
|
||||
];
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<AlertModal
|
||||
aria-label={i18n._(t`Associate role error`)}
|
||||
isOpen={error}
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={dismissError}
|
||||
>
|
||||
{i18n._(t`Failed to associate role`)}
|
||||
<ErrorDetail error={error} />
|
||||
</AlertModal>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Wizard
|
||||
isOpen={isOpen}
|
||||
title={title}
|
||||
steps={steps}
|
||||
onClose={onClose}
|
||||
onNext={({ id }) =>
|
||||
setStepIdReached(stepIdReached < id ? id : stepIdReached)
|
||||
}
|
||||
onSave={handleWizardSave}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(UserAndTeamAccessAdd);
|
||||
@ -0,0 +1,232 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { UsersAPI, JobTemplatesAPI } from '../../api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
import UserAndTeamAccessAdd from './UserAndTeamAccessAdd';
|
||||
|
||||
jest.mock('../../api/models/Teams');
|
||||
jest.mock('../../api/models/Users');
|
||||
jest.mock('../../api/models/JobTemplates');
|
||||
|
||||
describe('<UserAndTeamAccessAdd/>', () => {
|
||||
const resources = {
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Job Template Foo Bar',
|
||||
url: '/api/v2/job_template/1/',
|
||||
summary_fields: {
|
||||
object_roles: {
|
||||
admin_role: {
|
||||
description: 'Can manage all aspects of the job template',
|
||||
name: 'Admin',
|
||||
id: 164,
|
||||
},
|
||||
execute_role: {
|
||||
description: 'May run the job template',
|
||||
name: 'Execute',
|
||||
id: 165,
|
||||
},
|
||||
read_role: {
|
||||
description: 'May view settings for the job template',
|
||||
name: 'Read',
|
||||
id: 166,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
count: 1,
|
||||
},
|
||||
};
|
||||
let wrapper;
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<UserAndTeamAccessAdd
|
||||
apiModel={UsersAPI}
|
||||
isOpen
|
||||
onSave={() => {}}
|
||||
onClose={() => {}}
|
||||
title="Add user permissions"
|
||||
/>
|
||||
);
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('should mount properly', async () => {
|
||||
expect(wrapper.find('PFWizard').length).toBe(1);
|
||||
});
|
||||
test('should disable steps', async () => {
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem[text="Select items from list"]')
|
||||
.prop('isDisabled')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem[text="Select roles to apply"]')
|
||||
.prop('isDisabled')
|
||||
).toBe(true);
|
||||
await act(async () =>
|
||||
wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({
|
||||
fetchItems: JobTemplatesAPI.read,
|
||||
label: 'Job template',
|
||||
selectedResource: 'jobTemplate',
|
||||
searchColumns: [{ name: 'Name', key: 'name', isDefault: true }],
|
||||
sortColumns: [{ name: 'Name', key: 'name' }],
|
||||
})
|
||||
);
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('WizardNavItem[text="Add resource type"]').prop('isDisabled')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem[text="Select items from list"]')
|
||||
.prop('isDisabled')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem[text="Select roles to apply"]')
|
||||
.prop('isDisabled')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('should call api to associate role', async () => {
|
||||
JobTemplatesAPI.read.mockResolvedValue(resources);
|
||||
UsersAPI.associateRole.mockResolvedValue({});
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({
|
||||
fetchItems: JobTemplatesAPI.read,
|
||||
label: 'Job template',
|
||||
selectedResource: 'jobTemplate',
|
||||
searchColumns: [{ name: 'Name', key: 'name', isDefault: true }],
|
||||
sortColumns: [{ name: 'Name', key: 'name' }],
|
||||
})
|
||||
);
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
expect(JobTemplatesAPI.read).toHaveBeenCalledWith({
|
||||
order_by: 'name',
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
});
|
||||
|
||||
await waitForElement(wrapper, 'SelectResourceStep', el => el.length > 0);
|
||||
expect(JobTemplatesAPI.read).toHaveBeenCalled();
|
||||
await act(async () =>
|
||||
wrapper
|
||||
.find('CheckboxListItem')
|
||||
.first()
|
||||
.find('input[type="checkbox"]')
|
||||
.simulate('change', { target: { checked: true } })
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('RolesStep').length).toBe(1);
|
||||
|
||||
await act(async () =>
|
||||
wrapper
|
||||
.find('CheckboxCard')
|
||||
.first()
|
||||
.prop('onSelect')()
|
||||
);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
await expect(UsersAPI.associateRole).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should throw error', async () => {
|
||||
JobTemplatesAPI.read.mockResolvedValue(resources);
|
||||
UsersAPI.associateRole.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'post',
|
||||
url: '/api/v2/users/a/roles',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
status: 403,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: () => ({
|
||||
id: 'a',
|
||||
}),
|
||||
}));
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({
|
||||
fetchItems: JobTemplatesAPI.read,
|
||||
label: 'Job template',
|
||||
selectedResource: 'jobTemplate',
|
||||
searchColumns: [{ name: 'Name', key: 'name', isDefault: true }],
|
||||
sortColumns: [{ name: 'Name', key: 'name' }],
|
||||
})
|
||||
);
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
await waitForElement(wrapper, 'SelectResourceStep', el => el.length > 0);
|
||||
expect(JobTemplatesAPI.read).toHaveBeenCalled();
|
||||
await act(async () =>
|
||||
wrapper
|
||||
.find('CheckboxListItem')
|
||||
.first()
|
||||
.find('input[type="checkbox"]')
|
||||
.simulate('change', { target: { checked: true } })
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('RolesStep').length).toBe(1);
|
||||
|
||||
await act(async () =>
|
||||
wrapper
|
||||
.find('CheckboxCard')
|
||||
.first()
|
||||
.prop('onSelect')()
|
||||
);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
await expect(UsersAPI.associateRole).toHaveBeenCalled();
|
||||
wrapper.update();
|
||||
expect(wrapper.find('AlertModal').length).toBe(1);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,208 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
JobTemplatesAPI,
|
||||
WorkflowJobTemplatesAPI,
|
||||
CredentialsAPI,
|
||||
InventoriesAPI,
|
||||
ProjectsAPI,
|
||||
OrganizationsAPI,
|
||||
} from '../../api';
|
||||
|
||||
export default function getResourceAccessConfig(i18n) {
|
||||
return [
|
||||
{
|
||||
selectedResource: 'jobTemplate',
|
||||
label: i18n._(t`Job templates`),
|
||||
searchColumns: [
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Playbook name`),
|
||||
key: 'playbook',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created By (Username)`),
|
||||
key: 'created_by__username',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified By (Username)`),
|
||||
key: 'modified_by__username',
|
||||
},
|
||||
],
|
||||
sortColumns: [
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
],
|
||||
fetchItems: queryParams => JobTemplatesAPI.read(queryParams),
|
||||
},
|
||||
{
|
||||
selectedResource: 'workflowJobTemplate',
|
||||
label: i18n._(t`Workflow job templates`),
|
||||
searchColumns: [
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Playbook name`),
|
||||
key: 'playbook',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created By (Username)`),
|
||||
key: 'created_by__username',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified By (Username)`),
|
||||
key: 'modified_by__username',
|
||||
},
|
||||
],
|
||||
sortColumns: [
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
],
|
||||
fetchItems: queryParams => WorkflowJobTemplatesAPI.read(queryParams),
|
||||
},
|
||||
{
|
||||
selectedResource: 'credential',
|
||||
label: i18n._(t`Credentials`),
|
||||
searchColumns: [
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Type`),
|
||||
key: 'scm_type',
|
||||
options: [
|
||||
[``, i18n._(t`Manual`)],
|
||||
[`git`, i18n._(t`Git`)],
|
||||
[`hg`, i18n._(t`Mercurial`)],
|
||||
[`svn`, i18n._(t`Subversion`)],
|
||||
[`insights`, i18n._(t`Red Hat Insights`)],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Source Control URL`),
|
||||
key: 'scm_url',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified By (Username)`),
|
||||
key: 'modified_by__username',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created By (Username)`),
|
||||
key: 'created_by__username',
|
||||
},
|
||||
],
|
||||
sortColumns: [
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
],
|
||||
fetchItems: queryParams => CredentialsAPI.read(queryParams),
|
||||
},
|
||||
{
|
||||
selectedResource: 'inventory',
|
||||
label: i18n._(t`Inventories`),
|
||||
searchColumns: [
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created By (Username)`),
|
||||
key: 'created_by__username',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified By (Username)`),
|
||||
key: 'modified_by__username',
|
||||
},
|
||||
],
|
||||
sortColumns: [
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
],
|
||||
fetchItems: queryParams => InventoriesAPI.read(queryParams),
|
||||
},
|
||||
{
|
||||
selectedResource: 'project',
|
||||
label: i18n._(t`Projects`),
|
||||
searchColumns: [
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Type`),
|
||||
key: 'scm_type',
|
||||
options: [
|
||||
[``, i18n._(t`Manual`)],
|
||||
[`git`, i18n._(t`Git`)],
|
||||
[`hg`, i18n._(t`Mercurial`)],
|
||||
[`svn`, i18n._(t`Subversion`)],
|
||||
[`insights`, i18n._(t`Red Hat Insights`)],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Source Control URL`),
|
||||
key: 'scm_url',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified By (Username)`),
|
||||
key: 'modified_by__username',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created By (Username)`),
|
||||
key: 'created_by__username',
|
||||
},
|
||||
],
|
||||
sortColumns: [
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
],
|
||||
fetchItems: queryParams => ProjectsAPI.read(queryParams),
|
||||
},
|
||||
{
|
||||
selectedResource: 'organization',
|
||||
label: i18n._(t`Organizations`),
|
||||
searchColumns: [
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created By (Username)`),
|
||||
key: 'created_by__username',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified By (Username)`),
|
||||
key: 'modified_by__username',
|
||||
},
|
||||
],
|
||||
sortColumns: [
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
],
|
||||
fetchItems: queryParams => OrganizationsAPI.read(queryParams),
|
||||
},
|
||||
];
|
||||
}
|
||||
1
awx/ui_next/src/components/UserAndTeamAccessAdd/index.js
Normal file
1
awx/ui_next/src/components/UserAndTeamAccessAdd/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './UserAndTeamAccessAdd';
|
||||
@ -1,19 +1,18 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useLocation, useRouteMatch, useParams } from 'react-router-dom';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { Card } from '@patternfly/react-core';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { TeamsAPI } from '../../../api';
|
||||
|
||||
import useRequest from '../../../util/useRequest';
|
||||
import DataListToolbar from '../../../components/DataListToolbar';
|
||||
import PaginatedDataList, {
|
||||
ToolbarAddButton,
|
||||
} from '../../../components/PaginatedDataList';
|
||||
import PaginatedDataList from '../../../components/PaginatedDataList';
|
||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||
import TeamAccessListItem from './TeamAccessListItem';
|
||||
import UserAndTeamAccessAdd from '../../../components/UserAndTeamAccessAdd/UserAndTeamAccessAdd';
|
||||
|
||||
const QS_CONFIG = getQSConfig('team', {
|
||||
page: 1,
|
||||
@ -22,8 +21,8 @@ const QS_CONFIG = getQSConfig('team', {
|
||||
});
|
||||
|
||||
function TeamAccessList({ i18n }) {
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
const { search } = useLocation();
|
||||
const match = useRouteMatch();
|
||||
const { id } = useParams();
|
||||
|
||||
const {
|
||||
@ -57,6 +56,11 @@ function TeamAccessList({ i18n }) {
|
||||
fetchRoles();
|
||||
}, [fetchRoles]);
|
||||
|
||||
const saveRoles = () => {
|
||||
setIsWizardOpen(false);
|
||||
fetchRoles();
|
||||
};
|
||||
|
||||
const canAdd =
|
||||
options && Object.prototype.hasOwnProperty.call(options, 'POST');
|
||||
|
||||
@ -77,7 +81,7 @@ function TeamAccessList({ i18n }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading}
|
||||
@ -104,7 +108,17 @@ function TeamAccessList({ i18n }) {
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
...(canAdd
|
||||
? [<ToolbarAddButton key="add" linkTo={`${match.url}/add`} />]
|
||||
? [
|
||||
<Button
|
||||
key="add"
|
||||
aria-label={i18n._(t`Add resource roles`)}
|
||||
onClick={() => {
|
||||
setIsWizardOpen(true);
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>,
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
@ -117,13 +131,17 @@ function TeamAccessList({ i18n }) {
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
)}
|
||||
emptyStateControls={
|
||||
canAdd ? (
|
||||
<ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
{isWizardOpen && (
|
||||
<UserAndTeamAccessAdd
|
||||
apiModel={TeamsAPI}
|
||||
isOpen={isWizardOpen}
|
||||
onSave={saveRoles}
|
||||
onClose={() => setIsWizardOpen(false)}
|
||||
title={i18n._(t`Add team permissions`)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default withI18n()(TeamAccessList);
|
||||
|
||||
@ -141,4 +141,67 @@ describe('<TeamAccessList />', () => {
|
||||
'/inventories/smart_inventory/77/details'
|
||||
);
|
||||
});
|
||||
test('should not render add button', async () => {
|
||||
TeamsAPI.readRoleOptions.mockResolvedValueOnce({
|
||||
data: {},
|
||||
});
|
||||
|
||||
TeamsAPI.readRoles.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Admin',
|
||||
type: 'role',
|
||||
url: '/api/v2/roles/257/',
|
||||
summary_fields: {
|
||||
resource_name: 'template delete project',
|
||||
resource_id: 15,
|
||||
resource_type: 'job_template',
|
||||
resource_type_display_name: 'Job Template',
|
||||
user_capabilities: { unattach: true },
|
||||
},
|
||||
description: 'Can manage all aspects of the job template',
|
||||
},
|
||||
],
|
||||
count: 1,
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Route path="/teams/:id/access">
|
||||
<TeamAccessList />
|
||||
</Route>,
|
||||
{
|
||||
context: {
|
||||
router: {
|
||||
history,
|
||||
route: {
|
||||
location: history.location,
|
||||
match: { params: { id: 18 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||
expect(wrapper.find('Button[aria-label="Add resource roles"]').length).toBe(
|
||||
0
|
||||
);
|
||||
});
|
||||
test('should open and close wizard', async () => {
|
||||
waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||
await act(async () =>
|
||||
wrapper.find('Button[aria-label="Add resource roles"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('PFWizard').length).toBe(1);
|
||||
await act(async () =>
|
||||
wrapper.find("Button[aria-label='Close']").prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('PFWizard').length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
|
||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||
import { UsersAPI } from '../../../api';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
import PaginatedDataList, {
|
||||
ToolbarAddButton,
|
||||
} from '../../../components/PaginatedDataList';
|
||||
import PaginatedDataList from '../../../components/PaginatedDataList';
|
||||
import DatalistToolbar from '../../../components/DataListToolbar';
|
||||
import UserAccessListItem from './UserAccessListItem';
|
||||
import UserAndTeamAccessAdd from '../../../components/UserAndTeamAccessAdd/UserAndTeamAccessAdd';
|
||||
|
||||
const QS_CONFIG = getQSConfig('roles', {
|
||||
page: 1,
|
||||
@ -22,6 +23,7 @@ const QS_CONFIG = getQSConfig('roles', {
|
||||
function UserAccessList({ i18n }) {
|
||||
const { id } = useParams();
|
||||
const { search } = useLocation();
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
@ -55,6 +57,11 @@ function UserAccessList({ i18n }) {
|
||||
const canAdd =
|
||||
options && Object.prototype.hasOwnProperty.call(options, 'POST');
|
||||
|
||||
const saveRoles = () => {
|
||||
setIsWizardOpen(false);
|
||||
fetchRoles();
|
||||
};
|
||||
|
||||
const detailUrl = role => {
|
||||
const { resource_id, resource_type } = role.summary_fields;
|
||||
|
||||
@ -72,48 +79,71 @@ function UserAccessList({ i18n }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<PaginatedDataList
|
||||
contentError={error}
|
||||
hasContentLoading={isLoading}
|
||||
items={roles}
|
||||
itemCount={roleCount}
|
||||
pluralizedItemName={i18n._(t`User Roles`)}
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarSearchColumns={[
|
||||
{
|
||||
name: i18n._(t`Role`),
|
||||
key: 'role_field',
|
||||
isDefault: true,
|
||||
},
|
||||
]}
|
||||
toolbarSortColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'id',
|
||||
},
|
||||
]}
|
||||
renderItem={role => {
|
||||
return (
|
||||
<UserAccessListItem
|
||||
key={role.id}
|
||||
value={role.name}
|
||||
role={role}
|
||||
detailUrl={detailUrl(role)}
|
||||
onSelect={() => {}}
|
||||
isSelected={false}
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={error}
|
||||
hasContentLoading={isLoading}
|
||||
items={roles}
|
||||
itemCount={roleCount}
|
||||
pluralizedItemName={i18n._(t`User Roles`)}
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarSearchColumns={[
|
||||
{
|
||||
name: i18n._(t`Role`),
|
||||
key: 'role_field',
|
||||
isDefault: true,
|
||||
},
|
||||
]}
|
||||
toolbarSortColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'id',
|
||||
},
|
||||
]}
|
||||
renderItem={role => {
|
||||
return (
|
||||
<UserAccessListItem
|
||||
key={role.id}
|
||||
value={role.name}
|
||||
role={role}
|
||||
detailUrl={detailUrl(role)}
|
||||
onSelect={() => {}}
|
||||
isSelected={false}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
renderToolbar={props => (
|
||||
<DatalistToolbar
|
||||
{...props}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
...(canAdd
|
||||
? [
|
||||
<Button
|
||||
key="add"
|
||||
aria-label={i18n._(t`Add resource roles`)}
|
||||
onClick={() => {
|
||||
setIsWizardOpen(true);
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>,
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
renderToolbar={props => (
|
||||
<DatalistToolbar
|
||||
{...props}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
...(canAdd ? [<ToolbarAddButton key="add" linkTo="/" />] : []),
|
||||
]}
|
||||
)}
|
||||
/>
|
||||
{isWizardOpen && (
|
||||
<UserAndTeamAccessAdd
|
||||
apiModel={UsersAPI}
|
||||
isOpen={isWizardOpen}
|
||||
onSave={saveRoles}
|
||||
onClose={() => setIsWizardOpen(false)}
|
||||
title={i18n._(t`Add user permissions`)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default withI18n()(UserAccessList);
|
||||
|
||||
@ -29,6 +29,7 @@ describe('<UserAccessList />', () => {
|
||||
resource_type_display_name: 'Job Template',
|
||||
user_capabilities: { unattach: true },
|
||||
},
|
||||
description: 'Can manage all aspects of the job template',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@ -42,6 +43,7 @@ describe('<UserAccessList />', () => {
|
||||
resource_type_display_name: 'Job Template',
|
||||
user_capabilities: { unattach: true },
|
||||
},
|
||||
description: 'Can manage all aspects of the job template',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
@ -55,10 +57,11 @@ describe('<UserAccessList />', () => {
|
||||
resource_type_display_name: 'Credential',
|
||||
user_capabilities: { unattach: true },
|
||||
},
|
||||
description: 'May run the job template',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Update',
|
||||
name: 'Read',
|
||||
type: 'role',
|
||||
url: '/api/v2/roles/259/',
|
||||
summary_fields: {
|
||||
@ -68,6 +71,7 @@ describe('<UserAccessList />', () => {
|
||||
resource_type_display_name: 'Inventory',
|
||||
user_capabilities: { unattach: true },
|
||||
},
|
||||
description: 'May view settings for the job template',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
@ -75,15 +79,16 @@ describe('<UserAccessList />', () => {
|
||||
type: 'role',
|
||||
url: '/api/v2/roles/260/',
|
||||
summary_fields: {
|
||||
resource_name: 'Smart Inventory Foo',
|
||||
resource_name: 'Project Foo',
|
||||
resource_id: 77,
|
||||
resource_type: 'smart_inventory',
|
||||
resource_type_display_name: 'Inventory',
|
||||
resource_type: 'project',
|
||||
resource_type_display_name: 'Project',
|
||||
user_capabilities: { unattach: true },
|
||||
},
|
||||
description: 'Can manage all aspects of the job template',
|
||||
},
|
||||
],
|
||||
count: 4,
|
||||
count: 5,
|
||||
},
|
||||
});
|
||||
|
||||
@ -138,7 +143,86 @@ describe('<UserAccessList />', () => {
|
||||
'/inventories/inventory/76/details'
|
||||
);
|
||||
expect(wrapper.find('Link#userRole-6').prop('to')).toBe(
|
||||
'/inventories/smart_inventory/77/details'
|
||||
'/projects/77/details'
|
||||
);
|
||||
});
|
||||
test('should not render add button', async () => {
|
||||
UsersAPI.readRoleOptions.mockResolvedValueOnce({
|
||||
data: {},
|
||||
});
|
||||
|
||||
UsersAPI.readRoles.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Admin',
|
||||
type: 'role',
|
||||
url: '/api/v2/roles/257/',
|
||||
summary_fields: {
|
||||
resource_name: 'template delete project',
|
||||
resource_id: 15,
|
||||
resource_type: 'job_template',
|
||||
resource_type_display_name: 'Job Template',
|
||||
user_capabilities: { unattach: true },
|
||||
object_roles: {
|
||||
admin_role: {
|
||||
description: 'Can manage all aspects of the job template',
|
||||
name: 'Admin',
|
||||
id: 164,
|
||||
},
|
||||
execute_role: {
|
||||
description: 'May run the job template',
|
||||
name: 'Execute',
|
||||
id: 165,
|
||||
},
|
||||
read_role: {
|
||||
description: 'May view settings for the job template',
|
||||
name: 'Read',
|
||||
id: 166,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
count: 1,
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Route path="/users/:id/access">
|
||||
<UserAccessList />
|
||||
</Route>,
|
||||
{
|
||||
context: {
|
||||
router: {
|
||||
history,
|
||||
route: {
|
||||
location: history.location,
|
||||
match: { params: { id: 18 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||
expect(wrapper.find('Button[aria-label="Add resource roles"]').length).toBe(
|
||||
0
|
||||
);
|
||||
});
|
||||
test('should open and close wizard', async () => {
|
||||
waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||
await act(async () =>
|
||||
wrapper.find('Button[aria-label="Add resource roles"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('PFWizard').length).toBe(1);
|
||||
await act(async () =>
|
||||
wrapper.find("Button[aria-label='Close']").prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('PFWizard').length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user