diff --git a/awx/api/serializers.py b/awx/api/serializers.py
index 571edf57a9..631d8afeca 100644
--- a/awx/api/serializers.py
+++ b/awx/api/serializers.py
@@ -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
diff --git a/awx/ui_next/src/api/models/Teams.js b/awx/ui_next/src/api/models/Teams.js
index 585eb1086d..de2d3db077 100644
--- a/awx/ui_next/src/api/models/Teams.js
+++ b/awx/ui_next/src/api/models/Teams.js
@@ -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;
diff --git a/awx/ui_next/src/screens/Team/Team.jsx b/awx/ui_next/src/screens/Team/Team.jsx
index a702970c39..48cc13c02c 100644
--- a/awx/ui_next/src/screens/Team/Team.jsx
+++ b/awx/ui_next/src/screens/Team/Team.jsx
@@ -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 && (
- Coming soon :)
+
)}
diff --git a/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.jsx b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.jsx
new file mode 100644
index 0000000000..7d2cea1a72
--- /dev/null
+++ b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.jsx
@@ -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 (
+
+ (
+ ]
+ : []),
+ ]}
+ />
+ )}
+ renderItem={role => (
+ {}}
+ />
+ )}
+ emptyStateControls={
+ canAdd ? (
+
+ ) : null
+ }
+ />
+
+ );
+}
+export default withI18n()(TeamAccessList);
diff --git a/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.test.jsx b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.test.jsx
new file mode 100644
index 0000000000..12d8e9cf39
--- /dev/null
+++ b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.test.jsx
@@ -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('', () => {
+ 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(
+
+
+ ,
+ {
+ 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'
+ );
+ });
+});
diff --git a/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessListItem.jsx b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessListItem.jsx
new file mode 100644
index 0000000000..994c175ecd
--- /dev/null
+++ b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessListItem.jsx
@@ -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 (
+
+
+
+
+ {role.summary_fields.resource_name}
+
+ ,
+
+ {role.summary_fields && (
+ <>
+ {i18n._(t`Type`)}
+ {role.summary_fields.resource_type_display_name}
+ >
+ )}
+ ,
+
+ {role.name && (
+ <>
+ {i18n._(t`Role`)}
+ {role.name}
+ >
+ )}
+ ,
+ ]}
+ />
+
+
+ );
+}
+export default withI18n()(TeamAccessListItem);
diff --git a/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessListItem.test.jsx b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessListItem.test.jsx
new file mode 100644
index 0000000000..45d40e08a1
--- /dev/null
+++ b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessListItem.test.jsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import TeamAccessListItem from './TeamAccessListItem';
+
+describe('', () => {
+ 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(
+
+ );
+ });
+
+ 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');
+ });
+});
diff --git a/awx/ui_next/src/screens/Team/TeamAccess/index.js b/awx/ui_next/src/screens/Team/TeamAccess/index.js
new file mode 100644
index 0000000000..d249ad2afa
--- /dev/null
+++ b/awx/ui_next/src/screens/Team/TeamAccess/index.js
@@ -0,0 +1 @@
+export { default } from './TeamAccessList';