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:
parent
b4df8a73c8
commit
c79905ce32
7 changed files with 160 additions and 107 deletions
|
@ -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;
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue