Issues/921 pagination consumer groups (#1802)

* Add Pagination to the ConsumerGroups Page

* Consumer Groups test suites minor code modifications

* Consumer Groups test Search results code

* Consumer Groups test Search with Api request

* Consumer Groups Search Logic
This commit is contained in:
Mgrdich 2022-04-05 15:48:22 +04:00 committed by GitHub
parent b4df8a73c8
commit c79905ce32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 160 additions and 107 deletions

View file

@ -1,57 +1,29 @@
import React from 'react'; import React from 'react';
import { ClusterName } from 'redux/interfaces'; import { Switch } from 'react-router-dom';
import { Switch, useParams } from 'react-router-dom';
import PageLoader from 'components/common/PageLoader/PageLoader';
import Details from 'components/ConsumerGroups/Details/Details'; import Details from 'components/ConsumerGroups/Details/Details';
import ListContainer from 'components/ConsumerGroups/List/ListContainer'; import ListContainer from 'components/ConsumerGroups/List/ListContainer';
import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets'; import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets';
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
import {
fetchConsumerGroupsPaged,
getAreConsumerGroupsPagedFulfilled,
getConsumerGroupsOrderBy,
getConsumerGroupsSortOrder,
} from 'redux/reducers/consumerGroups/consumerGroupsSlice';
import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
const ConsumerGroups: React.FC = () => { const ConsumerGroups: React.FC = () => {
const dispatch = useAppDispatch(); return (
const { clusterName } = useParams<{ clusterName: ClusterName }>(); <Switch>
const isFetched = useAppSelector(getAreConsumerGroupsPagedFulfilled); <BreadcrumbRoute
const orderBy = useAppSelector(getConsumerGroupsOrderBy); exact
const sortOrder = useAppSelector(getConsumerGroupsSortOrder); path="/ui/clusters/:clusterName/consumer-groups"
React.useEffect(() => { component={ListContainer}
dispatch( />
fetchConsumerGroupsPaged({ <BreadcrumbRoute
clusterName, exact
orderBy: orderBy || undefined, path="/ui/clusters/:clusterName/consumer-groups/:consumerGroupID"
sortOrder, component={Details}
}) />
); <BreadcrumbRoute
}, [clusterName, orderBy, sortOrder, dispatch]); path="/ui/clusters/:clusterName/consumer-groups/:consumerGroupID/reset-offsets"
component={ResetOffsets}
if (isFetched) { />
return ( </Switch>
<Switch> );
<BreadcrumbRoute
exact
path="/ui/clusters/:clusterName/consumer-groups"
component={ListContainer}
/>
<BreadcrumbRoute
exact
path="/ui/clusters/:clusterName/consumer-groups/:consumerGroupID"
component={Details}
/>
<BreadcrumbRoute
path="/ui/clusters/:clusterName/consumer-groups/:consumerGroupID/reset-offsets"
component={ResetOffsets}
/>
</Switch>
);
}
return <PageLoader />;
}; };
export default ConsumerGroups; export default ConsumerGroups;

View file

@ -1,4 +1,5 @@
import React, { useMemo } from 'react'; import React from 'react';
import { useParams } from 'react-router-dom';
import PageHeading from 'components/common/PageHeading/PageHeading'; import PageHeading from 'components/common/PageHeading/PageHeading';
import Search from 'components/common/Search/Search'; import Search from 'components/common/Search/Search';
import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled'; import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
@ -14,12 +15,19 @@ import {
GroupIDCell, GroupIDCell,
StatusCell, StatusCell,
} from 'components/ConsumerGroups/List/ConsumerGroupsTableCells'; } from 'components/ConsumerGroups/List/ConsumerGroupsTableCells';
import usePagination from 'lib/hooks/usePagination';
import useSearch from 'lib/hooks/useSearch';
import { useAppDispatch } from 'lib/hooks/redux';
import { ClusterName } from 'redux/interfaces';
import { fetchConsumerGroupsPaged } from 'redux/reducers/consumerGroups/consumerGroupsSlice';
import PageLoader from 'components/common/PageLoader/PageLoader';
export interface Props { export interface Props {
consumerGroups: ConsumerGroupDetails[]; consumerGroups: ConsumerGroupDetails[];
orderBy: ConsumerGroupOrdering | null; orderBy: ConsumerGroupOrdering | null;
sortOrder: SortOrder; sortOrder: SortOrder;
totalPages: number; totalPages: number;
isFetched: boolean;
setConsumerGroupsSortOrderBy(orderBy: ConsumerGroupOrdering | null): void; setConsumerGroupsSortOrderBy(orderBy: ConsumerGroupOrdering | null): void;
} }
@ -28,23 +36,33 @@ const List: React.FC<Props> = ({
sortOrder, sortOrder,
orderBy, orderBy,
totalPages, totalPages,
isFetched,
setConsumerGroupsSortOrderBy, setConsumerGroupsSortOrderBy,
}) => { }) => {
const [searchText, setSearchText] = React.useState<string>(''); const { page, perPage } = usePagination();
const [searchText, handleSearchText] = useSearch();
const dispatch = useAppDispatch();
const { clusterName } = useParams<{ clusterName: ClusterName }>();
const tableData = useMemo(() => { React.useEffect(() => {
return consumerGroups.filter( dispatch(
(consumerGroup) => fetchConsumerGroupsPaged({
!searchText || consumerGroup?.groupId?.indexOf(searchText) >= 0 clusterName,
orderBy: orderBy || undefined,
sortOrder,
page,
perPage,
search: searchText,
})
); );
}, [searchText, consumerGroups]); }, [clusterName, orderBy, searchText, sortOrder, page, perPage, dispatch]);
const tableState = useTableState< const tableState = useTableState<
ConsumerGroupDetails, ConsumerGroupDetails,
string, string,
ConsumerGroupOrdering ConsumerGroupOrdering
>( >(
tableData, consumerGroups,
{ {
totalPages, totalPages,
idSelector: (consumerGroup) => consumerGroup.groupId, idSelector: (consumerGroup) => consumerGroup.groupId,
@ -56,9 +74,9 @@ const List: React.FC<Props> = ({
} }
); );
const handleInputChange = (search: string) => { if (!isFetched) {
setSearchText(search); return <PageLoader />;
}; }
return ( return (
<div> <div>
@ -67,7 +85,7 @@ const List: React.FC<Props> = ({
<Search <Search
placeholder="Search by Consumer Group ID" placeholder="Search by Consumer Group ID"
value={searchText} value={searchText}
handleSearch={handleInputChange} handleSearch={handleSearchText}
/> />
</ControlPanelWrapper> </ControlPanelWrapper>
<SmartTable <SmartTable
@ -75,6 +93,7 @@ const List: React.FC<Props> = ({
isFullwidth isFullwidth
placeholder="No active consumer groups" placeholder="No active consumer groups"
hoverable hoverable
paginated
> >
<TableColumn <TableColumn
title="Consumer Group ID" title="Consumer Group ID"

View file

@ -6,6 +6,7 @@ import {
getConsumerGroupsTotalPages, getConsumerGroupsTotalPages,
sortBy, sortBy,
selectAll, selectAll,
getAreConsumerGroupsPagedFulfilled,
} from 'redux/reducers/consumerGroups/consumerGroupsSlice'; } from 'redux/reducers/consumerGroups/consumerGroupsSlice';
import List from 'components/ConsumerGroups/List/List'; import List from 'components/ConsumerGroups/List/List';
@ -14,6 +15,7 @@ const mapStateToProps = (state: RootState) => ({
orderBy: getConsumerGroupsOrderBy(state), orderBy: getConsumerGroupsOrderBy(state),
sortOrder: getConsumerGroupsSortOrder(state), sortOrder: getConsumerGroupsSortOrder(state),
totalPages: getConsumerGroupsTotalPages(state), totalPages: getConsumerGroupsTotalPages(state),
isFetched: getAreConsumerGroupsPagedFulfilled(state),
}); });
const mapDispatchToProps = { const mapDispatchToProps = {

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import List, { Props } from 'components/ConsumerGroups/List/List'; import List, { Props } from 'components/ConsumerGroups/List/List';
import { screen, waitFor } from '@testing-library/react'; import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { render } from 'lib/testHelpers'; import { render } from 'lib/testHelpers';
import { consumerGroups as consumerGroupMock } from 'redux/reducers/consumerGroups/__test__/fixtures'; import { consumerGroups as consumerGroupMock } from 'redux/reducers/consumerGroups/__test__/fixtures';
@ -23,6 +23,7 @@ describe('List', () => {
sortOrder={sortOrder || SortOrder.ASC} sortOrder={sortOrder || SortOrder.ASC}
setConsumerGroupsSortOrderBy={setConsumerGroupsSortOrderBy || jest.fn()} setConsumerGroupsSortOrderBy={setConsumerGroupsSortOrderBy || jest.fn()}
totalPages={totalPages || 1} totalPages={totalPages || 1}
isFetched={'isFetched' in props ? !!props.isFetched : true}
/> />
); );
}; };
@ -41,38 +42,6 @@ describe('List', () => {
expect(screen.getByText('groupId2')).toBeInTheDocument(); expect(screen.getByText('groupId2')).toBeInTheDocument();
}); });
describe('when searched', () => {
it('renders only searched consumers', async () => {
await waitFor(() => {
userEvent.type(
screen.getByPlaceholderText('Search by Consumer Group ID'),
consumerGroupMock[0].groupId
);
});
expect(
screen.getByText(consumerGroupMock[0].groupId)
).toBeInTheDocument();
expect(
screen.getByText(consumerGroupMock[1].groupId)
).toBeInTheDocument();
});
it('renders will not render a list since not found in the list', async () => {
await waitFor(() => {
userEvent.type(
screen.getByPlaceholderText('Search by Consumer Group ID'),
'NotFoundedText'
);
});
await waitFor(() => {
expect(
screen.getByText(/No active consumer groups/i)
).toBeInTheDocument();
});
});
});
describe('Testing the Ordering', () => { describe('Testing the Ordering', () => {
it('should test the sort order functionality', async () => { it('should test the sort order functionality', async () => {
const thElement = screen.getByText(/consumer group id/i); const thElement = screen.getByText(/consumer group id/i);

View file

@ -6,38 +6,69 @@ import {
waitForElementToBeRemoved, waitForElementToBeRemoved,
} from '@testing-library/react'; } from '@testing-library/react';
import ConsumerGroups from 'components/ConsumerGroups/ConsumerGroups'; import ConsumerGroups from 'components/ConsumerGroups/ConsumerGroups';
import { consumerGroups } from 'redux/reducers/consumerGroups/__test__/fixtures'; import {
consumerGroups,
noConsumerGroupsResponse,
} from 'redux/reducers/consumerGroups/__test__/fixtures';
import { render } from 'lib/testHelpers'; import { render } from 'lib/testHelpers';
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
import { Route } from 'react-router'; import { Route, Router } from 'react-router';
import { ConsumerGroupOrdering, SortOrder } from 'generated-sources'; import { ConsumerGroupOrdering, SortOrder } from 'generated-sources';
import { createMemoryHistory } from 'history';
const clusterName = 'cluster1'; const clusterName = 'cluster1';
const renderComponent = () => const historyMock = createMemoryHistory({
initialEntries: [clusterConsumerGroupsPath(clusterName)],
});
const renderComponent = (history = historyMock) =>
render( render(
<Route path={clusterConsumerGroupsPath(':clusterName')}> <Router history={history}>
<ConsumerGroups /> <Route path={clusterConsumerGroupsPath(':clusterName')}>
</Route>, <ConsumerGroups />
</Route>
</Router>,
{ {
pathname: clusterConsumerGroupsPath(clusterName), pathname: clusterConsumerGroupsPath(clusterName),
} }
); );
describe('ConsumerGroup', () => { describe('ConsumerGroups', () => {
it('renders with initial state', async () => { it('renders with initial state', async () => {
renderComponent(); renderComponent();
expect(screen.getByRole('progressbar')).toBeInTheDocument(); expect(screen.getByRole('progressbar')).toBeInTheDocument();
}); });
describe('Fetching Mock', () => { describe('Default Route and Fetching Consumer Groups', () => {
const url = `/api/clusters/${clusterName}/consumer-groups/paged?orderBy=${ConsumerGroupOrdering.NAME}&sortOrder=${SortOrder.ASC}`; const url = `/api/clusters/${clusterName}/consumer-groups/paged`;
afterEach(() => { afterEach(() => {
fetchMock.reset(); fetchMock.reset();
}); });
it('renders empty table on no consumer group response', async () => {
fetchMock.getOnce(url, noConsumerGroupsResponse, {
query: {
orderBy: ConsumerGroupOrdering.NAME,
sortOrder: SortOrder.ASC,
},
});
renderComponent();
await waitFor(() => expect(fetchMock.calls().length).toBe(1));
expect(screen.getByRole('table')).toBeInTheDocument();
expect(screen.getByText('No active consumer groups')).toBeInTheDocument();
});
it('renders with 404 from consumer groups', async () => { it('renders with 404 from consumer groups', async () => {
const consumerGroupsMock = fetchMock.getOnce(url, 404); const consumerGroupsMock = fetchMock.getOnce(url, 404, {
query: {
orderBy: ConsumerGroupOrdering.NAME,
sortOrder: SortOrder.ASC,
},
});
renderComponent(); renderComponent();
@ -48,10 +79,19 @@ describe('ConsumerGroup', () => {
}); });
it('renders with 200 from consumer groups', async () => { it('renders with 200 from consumer groups', async () => {
const consumerGroupsMock = fetchMock.getOnce(url, { const consumerGroupsMock = fetchMock.getOnce(
pagedCount: 1, url,
consumerGroups, {
}); pagedCount: 1,
consumerGroups,
},
{
query: {
orderBy: ConsumerGroupOrdering.NAME,
sortOrder: SortOrder.ASC,
},
}
);
renderComponent(); renderComponent();
@ -60,6 +100,43 @@ describe('ConsumerGroup', () => {
expect(screen.getByText('Consumers')).toBeInTheDocument(); expect(screen.getByText('Consumers')).toBeInTheDocument();
expect(screen.getByRole('table')).toBeInTheDocument(); expect(screen.getByRole('table')).toBeInTheDocument();
expect(screen.getByText(consumerGroups[0].groupId)).toBeInTheDocument();
expect(screen.getByText(consumerGroups[1].groupId)).toBeInTheDocument();
});
it('renders with 200 from consumer groups with Searched Query ', async () => {
const searchResult = consumerGroups[0];
const searchText = searchResult.groupId;
const consumerGroupsMock = fetchMock.getOnce(
url,
{
pagedCount: 1,
consumerGroups: [searchResult],
},
{
query: {
orderBy: ConsumerGroupOrdering.NAME,
sortOrder: SortOrder.ASC,
search: searchText,
},
}
);
const mockedHistory = createMemoryHistory({
initialEntries: [
`${clusterConsumerGroupsPath(clusterName)}?q=${searchText}`,
],
});
renderComponent(mockedHistory);
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy());
expect(screen.getByText(searchText)).toBeInTheDocument();
expect(
screen.queryByText(consumerGroups[1].groupId)
).not.toBeInTheDocument();
}); });
}); });
}); });

View file

@ -25,6 +25,11 @@ export const consumerGroups = [
}, },
]; ];
export const noConsumerGroupsResponse = {
pageCount: 1,
consumerGroups: [],
};
export const consumerGroupsPage = { export const consumerGroupsPage = {
totalPages: 1, totalPages: 1,
consumerGroups, consumerGroups,

View file

@ -33,15 +33,24 @@ export const fetchConsumerGroupsPaged = createAsyncThunk<
clusterName: ClusterName; clusterName: ClusterName;
orderBy?: ConsumerGroupOrdering; orderBy?: ConsumerGroupOrdering;
sortOrder?: SortOrder; sortOrder?: SortOrder;
page?: number;
perPage?: number;
search: string;
} }
>( >(
'consumerGroups/fetchConsumerGroupsPaged', 'consumerGroups/fetchConsumerGroupsPaged',
async ({ clusterName, orderBy, sortOrder }, { rejectWithValue }) => { async (
{ clusterName, orderBy, sortOrder, page, perPage, search },
{ rejectWithValue }
) => {
try { try {
const response = await api.getConsumerGroupsPageRaw({ const response = await api.getConsumerGroupsPageRaw({
clusterName, clusterName,
orderBy, orderBy,
sortOrder, sortOrder,
page,
perPage,
search,
}); });
return await response.value(); return await response.value();
} catch (error) { } catch (error) {