diff --git a/__tests__/pages/Organizations/components/OrganizationTeamsList.test.jsx b/__tests__/pages/Organizations/components/OrganizationTeamsList.test.jsx new file mode 100644 index 0000000000..563636b9c9 --- /dev/null +++ b/__tests__/pages/Organizations/components/OrganizationTeamsList.test.jsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { MemoryRouter } from 'react-router-dom'; +import { I18nProvider } from '@lingui/react'; + +import OrganizationTeamsList from '../../../../src/pages/Organizations/components/OrganizationTeamsList'; + +const mockData = [ + { + id: 1, + name: 'boo', + url: '/foo/bar/' + } +]; + +describe('', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('initially renders succesfully', () => { + mount( + + + {}} + removeRole={() => {}} + /> + + + ); + }); + + test('api response data passed to component gets set to state properly', (done) => { + const wrapper = mount( + + + ({ data: { count: 1, results: mockData } })} + /> + + + ).find('OrganizationTeamsList'); + + setImmediate(() => { + expect(wrapper.state().results).toEqual(mockData); + done(); + }); + }); + + test('onSort being passed properly to DataListToolbar component', async (done) => { + const onSort = jest.spyOn(OrganizationTeamsList.prototype, 'onSort'); + const wrapper = mount( + + + ({ data: { count: 1, results: mockData } })} + /> + + + ).find('OrganizationTeamsList'); + expect(onSort).not.toHaveBeenCalled(); + + setImmediate(() => { + const rendered = wrapper.update(); + rendered.find('button[aria-label="Sort"]').simulate('click'); + expect(onSort).toHaveBeenCalled(); + done(); + }); + }); + + test('onSetPage calls getQueryParams fetchOrgTeamsList ', () => { + const spyQueryParams = jest.spyOn(OrganizationTeamsList.prototype, 'getQueryParams'); + const spyFetch = jest.spyOn(OrganizationTeamsList.prototype, 'fetchOrgTeamsList'); + const wrapper = mount( + + + ({ data: { count: 1, results: mockData } })} + /> + + + ).find('OrganizationTeamsList'); + wrapper.instance().onSetPage(2, 10); + expect(spyQueryParams).toHaveBeenCalled(); + expect(spyFetch).toHaveBeenCalled(); + wrapper.setState({ sortOrder: 'descending' }); + wrapper.instance().onSetPage(3, 5); + expect(spyQueryParams).toHaveBeenCalled(); + expect(spyFetch).toHaveBeenCalled(); + const queryParamCalls = spyQueryParams.mock.calls; + // make sure last two getQueryParams calls + // were called with the correct arguments + expect(queryParamCalls[queryParamCalls.length - 2][0]) + .toEqual({ order_by: 'name', page: 2, page_size: 10 }); + expect(queryParamCalls[queryParamCalls.length - 1][0]) + .toEqual({ order_by: '-name', page: 3, page_size: 5 }); + }); +}); diff --git a/__tests__/pages/Organizations/screens/Organization/OrganizationAccess.test.jsx b/__tests__/pages/Organizations/screens/Organization/OrganizationAccess.test.jsx index 915c077364..837eb3814e 100644 --- a/__tests__/pages/Organizations/screens/Organization/OrganizationAccess.test.jsx +++ b/__tests__/pages/Organizations/screens/Organization/OrganizationAccess.test.jsx @@ -8,7 +8,7 @@ const mockAPIAccessList = { foo: 'bar', }; -const mockGetOrganzationAccessList = () => Promise.resolve(mockAPIAccessList); +const mockGetOrganizationAccessList = () => Promise.resolve(mockAPIAccessList); const mockResponse = { status: 'success', @@ -25,7 +25,7 @@ describe('', () => { location={{ search: '', pathname: '/organizations/1/access' }} params={{}} api={{ - getOrganzationAccessList: jest.fn(), + getOrganizationAccessList: jest.fn(), }} /> @@ -40,7 +40,7 @@ describe('', () => { location={{ search: '', pathname: '/organizations/1/access' }} params={{}} api={{ - getOrganzationAccessList: mockGetOrganzationAccessList, + getOrganizationAccessList: mockGetOrganizationAccessList, disassociate: mockRemoveRole }} /> diff --git a/__tests__/pages/Organizations/screens/Organization/OrganizationTeams.test.jsx b/__tests__/pages/Organizations/screens/Organization/OrganizationTeams.test.jsx new file mode 100644 index 0000000000..58fe3c2996 --- /dev/null +++ b/__tests__/pages/Organizations/screens/Organization/OrganizationTeams.test.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { MemoryRouter } from 'react-router-dom'; + +import OrganizationTeams from '../../../../../src/pages/Organizations/screens/Organization/OrganizationTeams'; + +const mockAPITeamsList = { + foo: 'bar', +}; + +const mockGetOrganizationTeamsList = () => Promise.resolve(mockAPITeamsList); + +describe('', () => { + test('initially renders succesfully', () => { + mount( + + + + ); + }); + + test('passed methods as props are called appropriately', async () => { + const wrapper = mount( + + + + ).find('OrganizationTeams'); + const teamsList = await wrapper.instance().getOrgTeamsList(); + expect(teamsList).toEqual(mockAPITeamsList); + }); +}); diff --git a/src/api.js b/src/api.js index f87c51703e..b3f594cdd9 100644 --- a/src/api.js +++ b/src/api.js @@ -64,12 +64,18 @@ class APIClient { return this.http.post(API_ORGANIZATIONS, data); } - getOrganzationAccessList (id, params = {}) { + getOrganizationAccessList (id, params = {}) { const endpoint = `${API_ORGANIZATIONS}${id}/access_list/`; return this.http.get(endpoint, { params }); } + getOrganizationTeamsList (id, params = {}) { + const endpoint = `${API_ORGANIZATIONS}${id}/teams/`; + + return this.http.get(endpoint, { params }); + } + getOrganizationDetails (id) { const endpoint = `${API_ORGANIZATIONS}${id}/`; diff --git a/src/pages/Organizations/components/OrganizationTeamsList.jsx b/src/pages/Organizations/components/OrganizationTeamsList.jsx new file mode 100644 index 0000000000..60ab6a94f5 --- /dev/null +++ b/src/pages/Organizations/components/OrganizationTeamsList.jsx @@ -0,0 +1,216 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; + +import { + DataList, DataListItem, DataListCell, Text, + TextContent, TextVariants +} from '@patternfly/react-core'; + +import { I18n, i18nMark } from '@lingui/react'; +import { t } from '@lingui/macro'; + +import { + Link +} from 'react-router-dom'; + +import Pagination from '../../../components/Pagination'; +import DataListToolbar from '../../../components/DataListToolbar'; + +import { + parseQueryString, +} from '../../../qs'; + +const detailWrapperStyle = { + display: 'grid', + gridTemplateColumns: 'minmax(70px, max-content) minmax(60px, max-content)', +}; + +const detailLabelStyle = { + fontWeight: '700', + lineHeight: '24px', + marginRight: '20px', +}; + +class OrganizationTeamsList extends React.Component { + columns = [ + { name: i18nMark('Name'), key: 'name', isSortable: true }, + ]; + + defaultParams = { + page: 1, + page_size: 5, + order_by: 'name', + }; + + constructor (props) { + super(props); + + const { page, page_size } = this.getQueryParams(); + + this.state = { + page, + page_size, + count: 0, + sortOrder: 'ascending', + sortedColumnKey: 'name', + results: [], + }; + + this.fetchOrgTeamsList = this.fetchOrgTeamsList.bind(this); + this.onSetPage = this.onSetPage.bind(this); + this.onSort = this.onSort.bind(this); + this.getQueryParams = this.getQueryParams.bind(this); + } + + componentDidMount () { + const queryParams = this.getQueryParams(); + try { + this.fetchOrgTeamsList(queryParams); + } catch (error) { + this.setState({ error }); + } + } + + onSetPage (pageNumber, pageSize) { + const { sortOrder, sortedColumnKey } = this.state; + const page = parseInt(pageNumber, 10); + const page_size = parseInt(pageSize, 10); + let order_by = sortedColumnKey; + + // Preserve sort order when paginating + if (sortOrder === 'descending') { + order_by = `-${order_by}`; + } + + const queryParams = this.getQueryParams({ page, page_size, order_by }); + + this.fetchOrgTeamsList(queryParams); + } + + onSort (sortedColumnKey, sortOrder) { + const { page_size } = this.state; + + let order_by = sortedColumnKey; + + if (sortOrder === 'descending') { + order_by = `-${order_by}`; + } + + const queryParams = this.getQueryParams({ order_by, page_size }); + + this.fetchOrgTeamsList(queryParams); + } + + getQueryParams (overrides = {}) { + const { location } = this.props; + const { search } = location; + + const searchParams = parseQueryString(search.substring(1)); + + return Object.assign({}, this.defaultParams, searchParams, overrides); + } + + async fetchOrgTeamsList (queryParams) { + const { match, getTeamsList } = this.props; + + const { page, page_size, order_by } = queryParams; + + let sortOrder = 'ascending'; + let sortedColumnKey = order_by; + + if (order_by.startsWith('-')) { + sortOrder = 'descending'; + sortedColumnKey = order_by.substring(1); + } + + try { + const { data: + { count = 0, results = [] } + } = await getTeamsList(match.params.id, queryParams); + const pageCount = Math.ceil(count / page_size); + + const stateToUpdate = { + count, + page, + pageCount, + page_size, + sortOrder, + sortedColumnKey, + results + }; + + this.setState(stateToUpdate); + } catch (error) { + this.setState({ error }); + } + } + + render () { + const { + results, + error, + count, + page_size, + pageCount, + page, + sortedColumnKey, + sortOrder + } = this.state; + return ( + + {({ i18n }) => ( + + {!error && results.length <= 0 && ( +

Loading...

// TODO: replace with proper loading state + )} + {error && results.length <= 0 && ( + +
{error.message}
+ {error.response && ( +
{error.response.data.detail}
+ )} +
// TODO: replace with proper error handling + )} + {results.length > 0 && ( + + { }} + onSort={this.onSort} + /> + + {results.map(({ url, id, name }) => ( + + + + + {name} + + + + + ))} + + + + )} +
+ )} +
+ ); + } +} + +OrganizationTeamsList.propTypes = { + getTeamsList: PropTypes.func.isRequired, +}; + +export default OrganizationTeamsList; diff --git a/src/pages/Organizations/screens/Organization/Organization.jsx b/src/pages/Organizations/screens/Organization/Organization.jsx index 85b090ee73..33788d0890 100644 --- a/src/pages/Organizations/screens/Organization/Organization.jsx +++ b/src/pages/Organizations/screens/Organization/Organization.jsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import { I18n, i18nMark } from '@lingui/react'; -import { Trans, t } from '@lingui/macro'; +import { t } from '@lingui/macro'; import { Switch, Route, @@ -9,7 +9,6 @@ import { } from 'react-router-dom'; import { Card, - CardBody, CardHeader, PageSection } from '@patternfly/react-core'; @@ -18,6 +17,7 @@ import OrganizationAccess from './OrganizationAccess'; import OrganizationDetail from './OrganizationDetail'; import OrganizationEdit from './OrganizationEdit'; import OrganizationNotifications from './OrganizationNotifications'; +import OrganizationTeams from './OrganizationTeams'; import Tabs from '../../../../components/Tabs/Tabs'; import Tab from '../../../../components/Tabs/Tab'; @@ -157,7 +157,14 @@ class Organization extends Component { />

Teams

} + render={() => ( + + )} /> + ); + } +} + +export default OrganizationTeams;