mirror of
https://github.com/ansible/awx.git
synced 2024-10-30 22:21:13 +03:00
Teams Access List using Resource Access component
This commit is contained in:
parent
a070d57080
commit
8e27e0ce28
@ -29,7 +29,7 @@ class Teams extends Base {
|
||||
return this.http.options(`${this.baseUrl}${teamId}/roles/`);
|
||||
}
|
||||
|
||||
readUsersAccess(teamId, params) {
|
||||
readAccessList(teamId, params) {
|
||||
return this.http.get(`${this.baseUrl}${teamId}/access_list/`, {
|
||||
params,
|
||||
});
|
||||
@ -38,7 +38,6 @@ class Teams extends Base {
|
||||
readUsersAccessOptions(teamId) {
|
||||
return this.http.options(`${this.baseUrl}${teamId}/users/`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Teams;
|
||||
|
@ -17,7 +17,7 @@ import TeamDetail from './TeamDetail';
|
||||
import TeamEdit from './TeamEdit';
|
||||
import { TeamsAPI } from '../../api';
|
||||
import TeamAccessList from './TeamAccess';
|
||||
import TeamUsersList from './TeamUsers';
|
||||
import { ResourceAccessList } from '../../components/ResourceAccessList';
|
||||
|
||||
function Team({ i18n, setBreadcrumb }) {
|
||||
const [team, setTeam] = useState(null);
|
||||
@ -97,7 +97,7 @@ function Team({ i18n, setBreadcrumb }) {
|
||||
)}
|
||||
{team && (
|
||||
<Route path="/teams/:id/access">
|
||||
<TeamUsersList />
|
||||
<ResourceAccessList resource={team} apiModel={TeamsAPI} />
|
||||
</Route>
|
||||
)}
|
||||
{team && (
|
||||
|
@ -1,102 +0,0 @@
|
||||
import 'styled-components/macro';
|
||||
import React from 'react';
|
||||
import { string, func } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
DataListItem,
|
||||
DataListItemCells,
|
||||
DataListItemRow,
|
||||
Label as PFLabel,
|
||||
} from '@patternfly/react-core';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import DataListCell from '../../../components/DataListCell';
|
||||
|
||||
import { User } from '../../../types';
|
||||
|
||||
function TeamUserListItem({ user, disassociateRole, detailUrl, i18n }) {
|
||||
const labelId = `check-action-${user.id}`;
|
||||
const Label = styled.b`
|
||||
margin-right: 20px;
|
||||
`;
|
||||
const hasDirectRoles = user.summary_fields.direct_access.length > 0;
|
||||
const hasIndirectRoles = user.summary_fields.indirect_access.length > 0;
|
||||
return (
|
||||
<DataListItem key={user.id} aria-labelledby={labelId} id={`${user.id}`}>
|
||||
<DataListItemRow>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell aria-label={i18n._(t`username`)} key="username">
|
||||
<Link id={labelId} to={`${detailUrl}`}>
|
||||
<b>{user.username}</b>
|
||||
</Link>
|
||||
</DataListCell>,
|
||||
<DataListCell aria-label={i18n._(t`first name`)} key="first name">
|
||||
{user.first_name && (
|
||||
<>
|
||||
<Label>{i18n._(t`First`)}</Label>
|
||||
<span>{user.first_name}</span>
|
||||
</>
|
||||
)}
|
||||
</DataListCell>,
|
||||
<DataListCell aria-label={i18n._(t`last name`)} key="last name">
|
||||
{user.last_name && (
|
||||
<>
|
||||
<Label>{i18n._(t`Last`)}</Label>
|
||||
<span>{user.last}</span>
|
||||
</>
|
||||
)}
|
||||
</DataListCell>,
|
||||
<DataListCell aria-label={i18n._(t`roles`)} key="role">
|
||||
{hasDirectRoles && (
|
||||
<>
|
||||
<Label>{i18n._(t`Roles`)}</Label>
|
||||
<span>
|
||||
{user.summary_fields.direct_access.map(role =>
|
||||
role.role.name !== 'Read' ? (
|
||||
<PFLabel
|
||||
aria-label={role.role.name}
|
||||
key={role.role.id}
|
||||
role={role.role}
|
||||
onClose={() => disassociateRole(role.role)}
|
||||
>
|
||||
{role.role.name}
|
||||
</PFLabel>
|
||||
) : null
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</DataListCell>,
|
||||
<DataListCell
|
||||
aria-label={i18n._(t`indirect role`)}
|
||||
key="indirectRole"
|
||||
>
|
||||
{hasIndirectRoles && (
|
||||
<>
|
||||
<Label>{i18n._(t`Indirect Roles`)}</Label>
|
||||
<span>
|
||||
{user.summary_fields.indirect_access.map(role => (
|
||||
<PFLabel key={role.role.id} credential={role.role}>
|
||||
{role.role.name}
|
||||
</PFLabel>
|
||||
))}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
);
|
||||
}
|
||||
|
||||
TeamUserListItem.propTypes = {
|
||||
user: User.isRequired,
|
||||
detailUrl: string.isRequired,
|
||||
disassociateRole: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(TeamUserListItem);
|
@ -1,84 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
|
||||
import TeamUserListItem from './TeamUserListItem';
|
||||
|
||||
describe('<TeamUserListItem />', () => {
|
||||
const user = {
|
||||
id: 1,
|
||||
name: 'Team 1',
|
||||
summary_fields: {
|
||||
direct_access: [
|
||||
{
|
||||
role: {
|
||||
id: 40,
|
||||
name: 'Member',
|
||||
description: 'User is a member of the team',
|
||||
resource_name: ' Team 1 Org 0',
|
||||
resource_type: 'team',
|
||||
related: {
|
||||
team: '/api/v2/teams/1/',
|
||||
},
|
||||
user_capabilities: {
|
||||
unattach: true,
|
||||
},
|
||||
},
|
||||
descendant_roles: ['member_role', 'read_role'],
|
||||
},
|
||||
],
|
||||
indirect_access: [
|
||||
{
|
||||
role: {
|
||||
id: 2,
|
||||
name: 'Admin',
|
||||
description: 'Can manage all aspects of the organization',
|
||||
resource_name: ' Organization 0',
|
||||
resource_type: 'organization',
|
||||
related: {
|
||||
organization: '/api/v2/organizations/1/',
|
||||
},
|
||||
user_capabilities: {
|
||||
unattach: true,
|
||||
},
|
||||
},
|
||||
descendant_roles: ['admin_role', 'member_role', 'read_role'],
|
||||
},
|
||||
],
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
username: 'Casey',
|
||||
firstname: 'The',
|
||||
lastname: 'Cat',
|
||||
email: '',
|
||||
};
|
||||
test('initially renders succesfully', () => {
|
||||
mountWithContexts(
|
||||
<TeamUserListItem
|
||||
user={user}
|
||||
detailUrl="/users/1/details"
|
||||
disassociateRole={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
test('initially render prop items', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<TeamUserListItem
|
||||
user={user}
|
||||
detailUrl="/users/1/details"
|
||||
disassociateRole={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('DataListCell[aria-label="username"]').length).toBe(1);
|
||||
expect(wrapper.find('DataListCell[aria-label="first name"]').length).toBe(
|
||||
1
|
||||
);
|
||||
expect(wrapper.find('DataListCell[aria-label="last name"]').length).toBe(1);
|
||||
expect(wrapper.find('DataListCell[aria-label="roles"]').length).toBe(1);
|
||||
expect(
|
||||
wrapper.find('DataListCell[aria-label="indirect role"]').length
|
||||
).toBe(1);
|
||||
});
|
||||
});
|
@ -1,198 +0,0 @@
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import { useLocation, useRouteMatch, useParams } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { TeamsAPI, UsersAPI } from '../../../api';
|
||||
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
||||
import AlertModal from '../../../components/AlertModal';
|
||||
import DataListToolbar from '../../../components/DataListToolbar';
|
||||
import ErrorDetail from '../../../components/ErrorDetail';
|
||||
import PaginatedDataList, {
|
||||
ToolbarAddButton,
|
||||
} from '../../../components/PaginatedDataList';
|
||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||
|
||||
import TeamUserListItem from './TeamUserListItem';
|
||||
|
||||
const QS_CONFIG = getQSConfig('user', {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
order_by: 'username',
|
||||
});
|
||||
|
||||
function TeamUsersList({ i18n }) {
|
||||
const location = useLocation();
|
||||
const match = useRouteMatch();
|
||||
const { id: teamId } = useParams();
|
||||
const [roleToDisassociate, setRoleToDisassociate] = useState([]);
|
||||
|
||||
const {
|
||||
result: { users, itemCount, actions },
|
||||
error: contentError,
|
||||
isLoading,
|
||||
request: fetchRoles,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
const [response, actionsResponse] = await Promise.all([
|
||||
TeamsAPI.readUsersAccess(teamId, params),
|
||||
TeamsAPI.readUsersAccessOptions(teamId),
|
||||
]);
|
||||
return {
|
||||
users: response.data.results,
|
||||
itemCount: response.data.count,
|
||||
actions: actionsResponse.data.actions,
|
||||
};
|
||||
}, [location, teamId]),
|
||||
{
|
||||
users: [],
|
||||
itemCount: 0,
|
||||
actions: {},
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRoles();
|
||||
}, [fetchRoles]);
|
||||
|
||||
const {
|
||||
isLoading: isDeleteLoading,
|
||||
deleteItems: disassociateRole,
|
||||
deletionError,
|
||||
clearDeletionError,
|
||||
} = useDeleteItems(
|
||||
useCallback(async () => {
|
||||
UsersAPI.disassociateRole(
|
||||
roleToDisassociate[0].id,
|
||||
roleToDisassociate[1].id
|
||||
);
|
||||
}, [roleToDisassociate]),
|
||||
{
|
||||
qsConfig: QS_CONFIG,
|
||||
fetchItems: fetchRoles,
|
||||
}
|
||||
);
|
||||
|
||||
const handleRoleDisassociation = async () => {
|
||||
await disassociateRole();
|
||||
setRoleToDisassociate(null);
|
||||
};
|
||||
|
||||
const hasContentLoading = isDeleteLoading || isLoading;
|
||||
const canAdd = actions && actions.POST;
|
||||
return (
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={hasContentLoading}
|
||||
items={users}
|
||||
itemCount={itemCount}
|
||||
pluralizedItemName={i18n._(t`Users`)}
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarSearchColumns={[
|
||||
{
|
||||
name: i18n._(t`User Name`),
|
||||
key: 'username',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`First Name`),
|
||||
key: 'first_name',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Last Name`),
|
||||
key: 'last_name',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Email`),
|
||||
key: 'email',
|
||||
},
|
||||
]}
|
||||
toolbarSortColumns={[
|
||||
{
|
||||
name: i18n._(t`User Name`),
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`First Name`),
|
||||
key: 'first_name',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Last Name`),
|
||||
key: 'last_name',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Email`),
|
||||
key: 'email',
|
||||
},
|
||||
]}
|
||||
renderToolbar={props => (
|
||||
<DataListToolbar
|
||||
{...props}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
...(canAdd
|
||||
? [<ToolbarAddButton key="add" linkTo="/users/add" />]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
renderItem={user => (
|
||||
<TeamUserListItem
|
||||
key={user.id}
|
||||
user={user}
|
||||
detailUrl={`/users/${user.id}/details`}
|
||||
disassociateRole={role => setRoleToDisassociate([user, role])}
|
||||
/>
|
||||
)}
|
||||
emptyStateControls={
|
||||
canAdd ? (
|
||||
<ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
{roleToDisassociate?.length > 0 && (
|
||||
<AlertModal
|
||||
variant="danger"
|
||||
title={i18n._(t`Disassociate roles`)}
|
||||
isOpen={roleToDisassociate}
|
||||
onClose={() => setRoleToDisassociate(null)}
|
||||
actions={[
|
||||
<Button
|
||||
key="disassociate"
|
||||
variant="danger"
|
||||
aria-label={i18n._(t`confirm disassociation`)}
|
||||
onClick={() => handleRoleDisassociation()}
|
||||
>
|
||||
{i18n._(t`Disassociate`)}
|
||||
</Button>,
|
||||
<Button
|
||||
key="cancel"
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`cancel disassociation`)}
|
||||
onClick={() => setRoleToDisassociate(null)}
|
||||
>
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div>{i18n._(t`This action will disassociate the following:`)}</div>
|
||||
<span>{roleToDisassociate.name}</span>
|
||||
</AlertModal>
|
||||
)}
|
||||
<AlertModal
|
||||
isOpen={deletionError}
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={clearDeletionError}
|
||||
>
|
||||
{i18n._(t`Failed to disassociate one or more roles.`)}
|
||||
<ErrorDetail error={deletionError} />
|
||||
</AlertModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(TeamUsersList);
|
@ -1,261 +0,0 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { TeamsAPI, UsersAPI } from '../../../api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../../testUtils/enzymeHelpers';
|
||||
|
||||
import TeamUsersList from './TeamUsersList';
|
||||
|
||||
jest.mock('../../../api/models/Teams');
|
||||
jest.mock('../../../api/models/Users');
|
||||
|
||||
const teamUsersList = {
|
||||
data: {
|
||||
count: 3,
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'user',
|
||||
url: '',
|
||||
summary_fields: {
|
||||
direct_access: [],
|
||||
indirect_access: [
|
||||
{
|
||||
role: {
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
created: '2020-06-19T12:55:13.138692Z',
|
||||
username: 'admin',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: 'a@g.com',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: 'user',
|
||||
url: '',
|
||||
summary_fields: {
|
||||
direct_access: [
|
||||
{
|
||||
role: {
|
||||
id: 40,
|
||||
name: 'Member',
|
||||
user_capabilities: {
|
||||
unattach: true,
|
||||
},
|
||||
},
|
||||
descendant_roles: ['member_role', 'read_role'],
|
||||
},
|
||||
{
|
||||
role: {
|
||||
id: 41,
|
||||
name: 'Read',
|
||||
user_capabilities: {
|
||||
unattach: true,
|
||||
},
|
||||
},
|
||||
descendant_roles: ['member_role', 'read_role'],
|
||||
},
|
||||
],
|
||||
indirect_access: [],
|
||||
},
|
||||
created: '2020-06-19T13:01:44.183577Z',
|
||||
username: 'jt_admin',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'user',
|
||||
url: '',
|
||||
summary_fields: {
|
||||
direct_access: [
|
||||
{
|
||||
role: {
|
||||
id: 40,
|
||||
name: 'Alex',
|
||||
user_capabilities: {
|
||||
unattach: true,
|
||||
},
|
||||
},
|
||||
descendant_roles: ['member_role', 'read_role'],
|
||||
},
|
||||
{
|
||||
role: {
|
||||
id: 41,
|
||||
name: 'Read',
|
||||
user_capabilities: {
|
||||
unattach: true,
|
||||
},
|
||||
},
|
||||
descendant_roles: ['member_role', 'read_role'],
|
||||
},
|
||||
],
|
||||
indirect_access: [
|
||||
{
|
||||
role: {
|
||||
id: 2,
|
||||
name: 'Admin',
|
||||
user_capabilities: {
|
||||
unattach: true,
|
||||
},
|
||||
},
|
||||
descendant_roles: ['admin_role', 'member_role', 'read_role'],
|
||||
},
|
||||
],
|
||||
},
|
||||
created: '2020-06-19T13:01:43.674349Z',
|
||||
username: 'org_admin',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'user',
|
||||
url: '',
|
||||
summary_fields: {
|
||||
direct_access: [
|
||||
{
|
||||
role: {
|
||||
id: 40,
|
||||
name: 'Savannah',
|
||||
user_capabilities: {
|
||||
unattach: true,
|
||||
},
|
||||
},
|
||||
descendant_roles: ['member_role', 'read_role'],
|
||||
},
|
||||
{
|
||||
role: {
|
||||
id: 41,
|
||||
name: 'Read',
|
||||
user_capabilities: {
|
||||
unattach: true,
|
||||
},
|
||||
},
|
||||
descendant_roles: ['member_role', 'read_role'],
|
||||
},
|
||||
],
|
||||
indirect_access: [],
|
||||
},
|
||||
created: '2020-06-19T13:01:43.868499Z',
|
||||
username: 'org_member',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe('<TeamUsersList />', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
TeamsAPI.readUsersAccess = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: teamUsersList.data,
|
||||
})
|
||||
);
|
||||
TeamsAPI.readUsersAccessOptions = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {},
|
||||
POST: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should load and render users', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<TeamUsersList />);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('TeamUserListItem')).toHaveLength(4);
|
||||
});
|
||||
|
||||
test('should disassociate role', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<TeamUsersList />);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('Label[aria-label="Member"]').prop('onClose')({
|
||||
id: 1,
|
||||
name: 'Member',
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('AlertModal[title="Disassociate roles"]').length).toBe(
|
||||
1
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('Button[aria-label="confirm disassociation"]')
|
||||
.prop('onClick')();
|
||||
});
|
||||
|
||||
expect(UsersAPI.disassociateRole).toHaveBeenCalledTimes(1);
|
||||
expect(TeamsAPI.readUsersAccess).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('should show disassociation error', async () => {
|
||||
UsersAPI.disassociateRole.mockResolvedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'post',
|
||||
url: '/api/v2/users/1',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<TeamUsersList />);
|
||||
});
|
||||
waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('Label[aria-label="Member"]').prop('onClose')({
|
||||
id: 1,
|
||||
name: 'Member',
|
||||
});
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('AlertModal[title="Disassociate roles"]').length).toBe(
|
||||
1
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('Button[aria-label="confirm disassociation"]')
|
||||
.prop('onClick')();
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(UsersAPI.disassociateRole).toHaveBeenCalled();
|
||||
|
||||
const modal = wrapper.find('Modal');
|
||||
expect(modal).toHaveLength(1);
|
||||
expect(modal.prop('title')).toEqual('Error!');
|
||||
});
|
||||
});
|
@ -1,4 +0,0 @@
|
||||
export {
|
||||
default
|
||||
}
|
||||
from './TeamUsersList'
|
Loading…
Reference in New Issue
Block a user