Adds Teams Access List and tests

This commit is contained in:
Alex Corey 2020-05-12 11:37:49 -04:00
parent bb0abf37e0
commit 09e72bc0ae
8 changed files with 379 additions and 2 deletions

View File

@ -2306,6 +2306,7 @@ class RoleSerializer(BaseSerializer):
content_model = obj.content_type.model_class()
ret['summary_fields']['resource_type'] = get_type_for_model(content_model)
ret['summary_fields']['resource_type_display_name'] = content_model._meta.verbose_name.title()
ret['summary_fields']['resource_id'] = obj.object_id
return ret

View File

@ -7,7 +7,9 @@ class Teams extends Base {
}
associateRole(teamId, roleId) {
return this.http.post(`${this.baseUrl}${teamId}/roles/`, { id: roleId });
return this.http.post(`${this.baseUrl}${teamId}/roles/`, {
id: roleId,
});
}
disassociateRole(teamId, roleId) {
@ -16,6 +18,16 @@ class Teams extends Base {
disassociate: true,
});
}
readRoles(teamId, params) {
return this.http.get(`${this.baseUrl}${teamId}/roles/`, {
params,
});
}
readRoleOptions(teamId) {
return this.http.options(`${this.baseUrl}${teamId}/roles/`);
}
}
export default Teams;

View File

@ -17,6 +17,7 @@ import ContentError from '@components/ContentError';
import TeamDetail from './TeamDetail';
import TeamEdit from './TeamEdit';
import { TeamsAPI } from '@api';
import TeamAccessList from './TeamAccess';
function Team({ i18n, setBreadcrumb }) {
const [team, setTeam] = useState(null);
@ -98,7 +99,7 @@ function Team({ i18n, setBreadcrumb }) {
)}
{team && (
<Route path="/teams/:id/access">
<span>Coming soon :)</span>
<TeamAccessList />
</Route>
)}
<Route key="not-found" path="*">

View File

@ -0,0 +1,129 @@
import React, { useCallback, useEffect } from 'react';
import { useLocation, useRouteMatch, useParams } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { TeamsAPI } from '@api';
import { Card } from '@patternfly/react-core';
import useRequest from '@util/useRequest';
import DataListToolbar from '@components/DataListToolbar';
import PaginatedDataList, {
ToolbarAddButton,
} from '@components/PaginatedDataList';
import { getQSConfig, parseQueryString } from '@util/qs';
import TeamAccessListItem from './TeamAccessListItem';
const QS_CONFIG = getQSConfig('team', {
page: 1,
page_size: 20,
order_by: 'id',
});
function TeamAccessList({ i18n }) {
const { search } = useLocation();
const match = useRouteMatch();
const { id } = useParams();
const {
isLoading,
request: fetchRoles,
contentError,
result: { roleCount, roles, options },
} = useRequest(
useCallback(async () => {
const params = parseQueryString(QS_CONFIG, search);
const [
{
data: { results, count },
},
{
data: { actions },
},
] = await Promise.all([
TeamsAPI.readRoles(id, params),
TeamsAPI.readRoleOptions(id),
]);
return { roleCount: count, roles: results, options: actions };
}, [id, search]),
{
roles: [],
roleCount: 0,
}
);
useEffect(() => {
fetchRoles();
}, [fetchRoles]);
const canAdd =
options && Object.prototype.hasOwnProperty.call(options, 'POST');
const detailUrl = role => {
const { resource_id, resource_type } = role.summary_fields;
if (!role || !resource_type) {
return null;
}
if (resource_type?.includes('template')) {
return `/templates/${resource_type}/${resource_id}/details`;
}
if (resource_type?.includes('inventory')) {
return `/inventories/${resource_type}/${resource_id}/details`;
}
return `/${resource_type}s/${resource_id}/details`;
};
return (
<Card>
<PaginatedDataList
contentError={contentError}
hasContentLoading={isLoading}
items={roles}
itemCount={roleCount}
pluralizedItemName={i18n._(t`Teams`)}
qsConfig={QS_CONFIG}
toolbarSearchColumns={[
{
name: i18n._(t`Role`),
key: 'role_field',
isDefault: true,
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Name`),
key: 'id',
},
]}
renderToolbar={props => (
<DataListToolbar
{...props}
qsConfig={QS_CONFIG}
additionalControls={[
...(canAdd
? [<ToolbarAddButton key="add" linkTo={`${match.url}/add`} />]
: []),
]}
/>
)}
renderItem={role => (
<TeamAccessListItem
key={role.id}
role={role}
detailUrl={detailUrl(role)}
onSelect={() => {}}
/>
)}
emptyStateControls={
canAdd ? (
<ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
) : null
}
/>
</Card>
);
}
export default withI18n()(TeamAccessList);

View File

@ -0,0 +1,141 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { TeamsAPI } from '@api';
import { Route } from 'react-router-dom';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import TeamAccessList from './TeamAccessList';
jest.mock('@api/models/Teams');
describe('<TeamAccessList />', () => {
let wrapper;
let history;
beforeEach(async () => {
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 },
},
},
{
id: 3,
name: 'Admin',
type: 'role',
url: '/api/v2/roles/257/',
summary_fields: {
resource_name: 'template delete project',
resource_id: 16,
resource_type: 'workflow_job_template',
resource_type_display_name: 'Job Template',
user_capabilities: { unattach: true },
},
},
{
id: 4,
name: 'Execute',
type: 'role',
url: '/api/v2/roles/258/',
summary_fields: {
resource_name: 'Credential Bar',
resource_id: 75,
resource_type: 'credential',
resource_type_display_name: 'Credential',
user_capabilities: { unattach: true },
},
},
{
id: 5,
name: 'Update',
type: 'role',
url: '/api/v2/roles/259/',
summary_fields: {
resource_name: 'Inventory Foo',
resource_id: 76,
resource_type: 'inventory',
resource_type_display_name: 'Inventory',
user_capabilities: { unattach: true },
},
},
{
id: 6,
name: 'Admin',
type: 'role',
url: '/api/v2/roles/260/',
summary_fields: {
resource_name: 'Smart Inventory Foo',
resource_id: 77,
resource_type: 'smart_inventory',
resource_type_display_name: 'Inventory',
user_capabilities: { unattach: true },
},
},
],
count: 4,
},
});
TeamsAPI.readRoleOptions.mockResolvedValue({
data: { actions: { POST: { id: 1, disassociate: true } } },
});
history = createMemoryHistory({
initialEntries: ['/teams/18/access'],
});
await act(async () => {
wrapper = mountWithContexts(
<Route path="/teams/:id/access">
<TeamAccessList />
</Route>,
{
context: {
router: {
history,
route: {
location: history.location,
match: { params: { id: 18 } },
},
},
},
}
);
});
});
afterEach(() => {
jest.clearAllMocks();
wrapper.unmount();
});
test('should render properly', async () => {
expect(wrapper.find('TeamAccessList').length).toBe(1);
});
test('should create proper detailUrl', async () => {
waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
expect(wrapper.find(`Link#teamRole-2`).prop('to')).toBe(
'/templates/job_template/15/details'
);
expect(wrapper.find(`Link#teamRole-3`).prop('to')).toBe(
'/templates/workflow_job_template/16/details'
);
expect(wrapper.find('Link#teamRole-4').prop('to')).toBe(
'/credentials/75/details'
);
expect(wrapper.find('Link#teamRole-5').prop('to')).toBe(
'/inventories/inventory/76/details'
);
expect(wrapper.find('Link#teamRole-6').prop('to')).toBe(
'/inventories/smart_inventory/77/details'
);
});
});

View File

@ -0,0 +1,47 @@
import React from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
DataListItem,
DataListItemCells,
DataListItemRow,
} from '@patternfly/react-core';
import DataListCell from '@components/DataListCell';
import { Link } from 'react-router-dom';
function TeamAccessListItem({ role, i18n, detailUrl }) {
const labelId = `teamRole-${role.id}`;
return (
<DataListItem key={role.id} aria-labelledby={labelId} id={`${role.id}`}>
<DataListItemRow>
<DataListItemCells
dataListCells={[
<DataListCell key="name" aria-label={i18n._(t`resource name`)}>
<Link to={`${detailUrl}`} id={labelId}>
<b>{role.summary_fields.resource_name}</b>
</Link>
</DataListCell>,
<DataListCell key="type" aria-label={i18n._(t`resource type`)}>
{role.summary_fields && (
<>
<b css="margin-right: 24px">{i18n._(t`Type`)}</b>
{role.summary_fields.resource_type_display_name}
</>
)}
</DataListCell>,
<DataListCell key="role" aria-label={i18n._(t`resource role`)}>
{role.name && (
<>
<b css="margin-right: 24px">{i18n._(t`Role`)}</b>
{role.name}
</>
)}
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
);
}
export default withI18n()(TeamAccessListItem);

View File

@ -0,0 +1,45 @@
import React from 'react';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import TeamAccessListItem from './TeamAccessListItem';
describe('<TeamAccessListItem/>', () => {
let wrapper;
const role = {
id: 1,
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 },
},
};
beforeEach(() => {
wrapper = mountWithContexts(
<TeamAccessListItem
role={role}
detailUrl="/templates/job_template/15/details"
/>
);
});
test('should mount properly', () => {
expect(wrapper.length).toBe(1);
});
test('should render proper list item data', () => {
expect(
wrapper.find('PFDataListCell[aria-label="resource name"]').text()
).toBe('template delete project');
expect(
wrapper.find('PFDataListCell[aria-label="resource type"]').text()
).toContain('Job Template');
expect(
wrapper.find('PFDataListCell[aria-label="resource role"]').text()
).toContain('Admin');
});
});

View File

@ -0,0 +1 @@
export { default } from './TeamAccessList';