diff --git a/awx/ui_next/src/api/models/Users.js b/awx/ui_next/src/api/models/Users.js index b98cf45cae..12eb74c4a6 100644 --- a/awx/ui_next/src/api/models/Users.js +++ b/awx/ui_next/src/api/models/Users.js @@ -34,6 +34,16 @@ class Users extends Base { readRoleOptions(userId) { return this.http.options(`${this.baseUrl}${userId}/roles/`); } + + readTeams(userId, params) { + return this.http.get(`${this.baseUrl}${userId}/teams/`, { + params, + }); + } + + readTeamsOptions(userId) { + return this.http.options(`${this.baseUrl}${userId}/teams/`); + } } export default Users; diff --git a/awx/ui_next/src/screens/Team/TeamList/TeamListItem.jsx b/awx/ui_next/src/screens/Team/TeamList/TeamListItem.jsx index f088dede27..47b2b4011c 100644 --- a/awx/ui_next/src/screens/Team/TeamList/TeamListItem.jsx +++ b/awx/ui_next/src/screens/Team/TeamList/TeamListItem.jsx @@ -58,7 +58,7 @@ class TeamListItem extends React.Component { {team.summary_fields.organization && ( - {i18n._(t`Organization`)} + {i18n._(t`Organization`)}{' '} diff --git a/awx/ui_next/src/screens/User/User.jsx b/awx/ui_next/src/screens/User/User.jsx index 982ae0de5e..5e195da2f3 100644 --- a/awx/ui_next/src/screens/User/User.jsx +++ b/awx/ui_next/src/screens/User/User.jsx @@ -108,7 +108,7 @@ function User({ i18n, setBreadcrumb }) { - + {user && ( diff --git a/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx b/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx index f45c9a5b93..bc01af942d 100644 --- a/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx +++ b/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx @@ -1,7 +1,5 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; import { DataListItemCells, DataListItemRow, @@ -9,14 +7,18 @@ import { } from '@patternfly/react-core'; import DataListCell from '../../../components/DataListCell'; -function UserOrganizationListItem({ organization, i18n }) { +export default function UserOrganizationListItem({ organization }) { + const labelId = `organization-${organization.id}`; return ( - + - + {organization.name} , @@ -29,5 +31,3 @@ function UserOrganizationListItem({ organization, i18n }) { ); } - -export default withI18n()(UserOrganizationListItem); diff --git a/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx b/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx new file mode 100644 index 0000000000..d7a902b7e3 --- /dev/null +++ b/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx @@ -0,0 +1,81 @@ +import React, { useCallback, useEffect } from 'react'; +import { withI18n } from '@lingui/react'; +import { useLocation, useParams } from 'react-router-dom'; +import { t } from '@lingui/macro'; + +import PaginatedDataList from '../../../components/PaginatedDataList'; +import useRequest from '../../../util/useRequest'; +import { UsersAPI } from '../../../api'; +import { getQSConfig, parseQueryString } from '../../../util/qs'; +import UserTeamListItem from './UserTeamListItem'; + +const QS_CONFIG = getQSConfig('teams', { + page: 1, + page_size: 20, + order_by: 'name', +}); + +function UserTeamList({ i18n }) { + const location = useLocation(); + const { id: userId } = useParams(); + + const { + result: { teams, count }, + error: contentError, + isLoading, + request: fetchOrgs, + } = useRequest( + useCallback(async () => { + const params = parseQueryString(QS_CONFIG, location.search); + const { + data: { results, count: teamCount }, + } = await UsersAPI.readTeams(userId, params); + return { + teams: results, + count: teamCount, + }; + }, [userId, location.search]), + { + teams: [], + count: 0, + } + ); + + useEffect(() => { + fetchOrgs(); + }, [fetchOrgs]); + + return ( + ( + {}} + isSelected={false} + /> + )} + toolbarSearchColumns={[ + { + name: i18n._(t`Name`), + key: 'name', + isDefault: true, + }, + { + name: i18n._(t`Organization`), + key: 'organization__name', + }, + ]} + /> + ); +} + +export default withI18n()(UserTeamList); diff --git a/awx/ui_next/src/screens/User/UserTeams/UserTeamList.test.jsx b/awx/ui_next/src/screens/User/UserTeams/UserTeamList.test.jsx new file mode 100644 index 0000000000..caac6b0c5f --- /dev/null +++ b/awx/ui_next/src/screens/User/UserTeams/UserTeamList.test.jsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { UsersAPI } from '../../../api'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; + +import UserTeamList from './UserTeamList'; + +jest.mock('../../../api'); + +const mockAPIUserTeamList = { + data: { + count: 3, + results: [ + { + name: 'Team 0', + id: 1, + url: '/teams/1', + summary_fields: { + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + { + name: 'Team 1', + id: 2, + url: '/teams/2', + summary_fields: { + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + { + name: 'Team 2', + id: 3, + url: '/teams/3', + summary_fields: { + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + ], + }, + isModalOpen: false, + warningTitle: 'title', + warningMsg: 'message', +}; + +describe('', () => { + beforeEach(() => { + UsersAPI.readTeams = jest.fn(() => + Promise.resolve({ + data: mockAPIUserTeamList.data, + }) + ); + UsersAPI.readOptions = jest.fn(() => + Promise.resolve({ + data: { + actions: { + GET: {}, + POST: {}, + }, + }, + }) + ); + }); + + test('should load and render teams', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + + expect(wrapper.find('UserTeamListItem')).toHaveLength(3); + }); +}); diff --git a/awx/ui_next/src/screens/User/UserTeams/UserTeamListItem.jsx b/awx/ui_next/src/screens/User/UserTeams/UserTeamListItem.jsx new file mode 100644 index 0000000000..41f4429879 --- /dev/null +++ b/awx/ui_next/src/screens/User/UserTeams/UserTeamListItem.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { + DataListItemCells, + DataListItemRow, + DataListItem, +} from '@patternfly/react-core'; +import DataListCell from '../../../components/DataListCell'; + +function UserTeamListItem({ team, i18n }) { + return ( + + + + + {team.name} + + , + + {team.summary_fields.organization && ( + <> + {i18n._(t`Organization`)}{' '} + + {team.summary_fields.organization.name} + + + )} + , + {team.description}, + ]} + /> + + + ); +} + +export default withI18n()(UserTeamListItem); diff --git a/awx/ui_next/src/screens/User/UserTeams/UserTeamListItem.test.jsx b/awx/ui_next/src/screens/User/UserTeams/UserTeamListItem.test.jsx new file mode 100644 index 0000000000..5816622eb2 --- /dev/null +++ b/awx/ui_next/src/screens/User/UserTeams/UserTeamListItem.test.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { I18nProvider } from '@lingui/react'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import UserTeamListItem from './UserTeamListItem'; + +describe('', () => { + test('should render item', () => { + const wrapper = mountWithContexts( + + + {}} + /> + + + ); + + const cells = wrapper.find('DataListCell'); + expect(cells).toHaveLength(3); + expect(cells.at(0).text()).toEqual('Team 1'); + expect(cells.at(1).text()).toEqual('Organization The Org'); + expect(cells.at(2).text()).toEqual('something something team'); + }); +}); diff --git a/awx/ui_next/src/screens/User/UserTeams/UserTeams.jsx b/awx/ui_next/src/screens/User/UserTeams/UserTeams.jsx index 5d342e00f2..65ddf670e5 100644 --- a/awx/ui_next/src/screens/User/UserTeams/UserTeams.jsx +++ b/awx/ui_next/src/screens/User/UserTeams/UserTeams.jsx @@ -1,10 +1,6 @@ -import React, { Component } from 'react'; -import { CardBody } from '../../../components/Card'; +import React from 'react'; +import UserTeamList from './UserTeamList'; -class UserAdd extends Component { - render() { - return Coming soon :); - } +export default function UserTeams() { + return ; } - -export default UserAdd;