diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx index 9049a19f7e..843e4e5bdf 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx @@ -3,22 +3,32 @@ import { ClusterName } from 'redux/interfaces'; import { Switch, useParams } from 'react-router-dom'; import PageLoader from 'components/common/PageLoader/PageLoader'; import Details from 'components/ConsumerGroups/Details/Details'; -import List from 'components/ConsumerGroups/List/List'; +import ListContainer from 'components/ConsumerGroups/List/ListContainer'; import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets'; import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; import { - fetchConsumerGroups, - getAreConsumerGroupsFulfilled, + fetchConsumerGroupsPaged, + getAreConsumerGroupsPagedFulfilled, + getConsumerGroupsOrderBy, + getConsumerGroupsSortOrder, } from 'redux/reducers/consumerGroups/consumerGroupsSlice'; import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; const ConsumerGroups: React.FC = () => { const dispatch = useAppDispatch(); const { clusterName } = useParams<{ clusterName: ClusterName }>(); - const isFetched = useAppSelector(getAreConsumerGroupsFulfilled); + const isFetched = useAppSelector(getAreConsumerGroupsPagedFulfilled); + const orderBy = useAppSelector(getConsumerGroupsOrderBy); + const sortOrder = useAppSelector(getConsumerGroupsSortOrder); React.useEffect(() => { - dispatch(fetchConsumerGroups(clusterName)); - }, [clusterName, dispatch]); + dispatch( + fetchConsumerGroupsPaged({ + clusterName, + orderBy: orderBy || undefined, + sortOrder, + }) + ); + }, [clusterName, orderBy, sortOrder, dispatch]); if (isFetched) { return ( @@ -26,7 +36,7 @@ const ConsumerGroups: React.FC = () => { > = ({ + dataItem, +}) => { + return {dataItem.state}; +}; + +export const GroupIDCell: React.FC> = ({ + dataItem: { groupId }, +}) => { + return ( + + {groupId} + + ); +}; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx index 9cd3769585..6fa0b9401f 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx @@ -1,18 +1,61 @@ -import React from 'react'; -import { Table } from 'components/common/table/Table/Table.styled'; -import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; +import React, { useMemo } from 'react'; import PageHeading from 'components/common/PageHeading/PageHeading'; import Search from 'components/common/Search/Search'; import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled'; -import { useAppSelector } from 'lib/hooks/redux'; -import { selectAll } from 'redux/reducers/consumerGroups/consumerGroupsSlice'; +import { + ConsumerGroupDetails, + ConsumerGroupOrdering, + SortOrder, +} from 'generated-sources'; +import { useTableState } from 'lib/hooks/useTableState'; +import { SmartTable } from 'components/common/SmartTable/SmartTable'; +import { TableColumn } from 'components/common/SmartTable/TableColumn'; +import { + GroupIDCell, + StatusCell, +} from 'components/ConsumerGroups/List/ConsumerGroupsTableCells'; -import ListItem from './ListItem'; +export interface Props { + consumerGroups: ConsumerGroupDetails[]; + orderBy: ConsumerGroupOrdering | null; + sortOrder: SortOrder; + totalPages: number; + setConsumerGroupsSortOrderBy(orderBy: ConsumerGroupOrdering | null): void; +} -const List: React.FC = () => { - const consumerGroups = useAppSelector(selectAll); +const List: React.FC = ({ + consumerGroups, + sortOrder, + orderBy, + totalPages, + setConsumerGroupsSortOrderBy, +}) => { const [searchText, setSearchText] = React.useState(''); + const tableData = useMemo(() => { + return consumerGroups.filter( + (consumerGroup) => + !searchText || consumerGroup?.groupId?.indexOf(searchText) >= 0 + ); + }, [searchText, consumerGroups]); + + const tableState = useTableState< + ConsumerGroupDetails, + string, + ConsumerGroupOrdering + >( + tableData, + { + totalPages, + idSelector: (consumerGroup) => consumerGroup.groupId, + }, + { + handleOrderBy: setConsumerGroupsSortOrderBy, + orderBy, + sortOrder, + } + ); + const handleInputChange = (search: string) => { setSearchText(search); }; @@ -27,36 +70,31 @@ const List: React.FC = () => { handleSearch={handleInputChange} /> - - - - - - - - - - - - - {consumerGroups - .filter( - (consumerGroup) => - !searchText || consumerGroup?.groupId?.indexOf(searchText) >= 0 - ) - .map((consumerGroup) => ( - - ))} - {consumerGroups.length === 0 && ( - - - - )} - -
No active consumer groups
+ + + + + + + + ); }; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/ListContainer.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/ListContainer.tsx new file mode 100644 index 0000000000..13cd4b0eb0 --- /dev/null +++ b/kafka-ui-react-app/src/components/ConsumerGroups/List/ListContainer.tsx @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import { RootState } from 'redux/interfaces'; +import { + getConsumerGroupsOrderBy, + getConsumerGroupsSortOrder, + getConsumerGroupsTotalPages, + sortBy, + selectAll, +} from 'redux/reducers/consumerGroups/consumerGroupsSlice'; +import List from 'components/ConsumerGroups/List/List'; + +const mapStateToProps = (state: RootState) => ({ + consumerGroups: selectAll(state), + orderBy: getConsumerGroupsOrderBy(state), + sortOrder: getConsumerGroupsSortOrder(state), + totalPages: getConsumerGroupsTotalPages(state), +}); + +const mapDispatchToProps = { + setConsumerGroupsSortOrderBy: sortBy, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(List); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ConsumerGroupsTableCells.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ConsumerGroupsTableCells.spec.tsx new file mode 100644 index 0000000000..2da6fc6613 --- /dev/null +++ b/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ConsumerGroupsTableCells.spec.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { render } from 'lib/testHelpers'; +import { + GroupIDCell, + StatusCell, +} from 'components/ConsumerGroups/List/ConsumerGroupsTableCells'; +import { TableState } from 'lib/hooks/useTableState'; +import { ConsumerGroup, ConsumerGroupState } from 'generated-sources'; +import { screen } from '@testing-library/react'; + +describe('Consumer Groups Table Cells', () => { + const consumerGroup: ConsumerGroup = { + groupId: 'groupId', + members: 1, + topics: 1, + simple: true, + state: ConsumerGroupState.STABLE, + coordinator: { + id: 6598, + }, + }; + const mockTableState: TableState = { + data: [consumerGroup], + selectedIds: new Set([]), + idSelector: jest.fn(), + isRowSelectable: jest.fn(), + selectedCount: 0, + setRowsSelection: jest.fn(), + toggleSelection: jest.fn(), + }; + + describe('StatusCell', () => { + it('should Tag props render normally', () => { + render( + + ); + const linkElement = screen.getByRole('link'); + expect(linkElement).toBeInTheDocument(); + expect(linkElement).toHaveAttribute( + 'href', + `/consumer-groups/${consumerGroup.groupId}` + ); + }); + }); + + describe('GroupIdCell', () => { + it('should GroupIdCell props render normally', () => { + render( + + ); + expect( + screen.getByText(consumerGroup.state as string) + ).toBeInTheDocument(); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/List.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/List.spec.tsx index d0e4ea8f21..69e24255e6 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/List.spec.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/List.spec.tsx @@ -1,27 +1,40 @@ import React from 'react'; -import List from 'components/ConsumerGroups/List/List'; +import List, { Props } from 'components/ConsumerGroups/List/List'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { render } from 'lib/testHelpers'; -import { store } from 'redux/store'; -import { fetchConsumerGroups } from 'redux/reducers/consumerGroups/consumerGroupsSlice'; -import { consumerGroups } from 'redux/reducers/consumerGroups/__test__/fixtures'; +import { consumerGroups as consumerGroupMock } from 'redux/reducers/consumerGroups/__test__/fixtures'; +import { ConsumerGroupOrdering, SortOrder } from 'generated-sources'; +import theme from 'theme/theme'; describe('List', () => { - beforeEach(() => render(, { store })); + const setUpComponent = (props: Partial = {}) => { + const { + consumerGroups, + orderBy, + sortOrder, + totalPages, + setConsumerGroupsSortOrderBy, + } = props; + return render( + + ); + }; it('renders empty table', () => { + setUpComponent(); expect(screen.getByRole('table')).toBeInTheDocument(); expect(screen.getByText('No active consumer groups')).toBeInTheDocument(); }); describe('consumerGroups are fetched', () => { - beforeEach(() => { - store.dispatch({ - type: fetchConsumerGroups.fulfilled.type, - payload: consumerGroups, - }); - }); + beforeEach(() => setUpComponent({ consumerGroups: consumerGroupMock })); it('renders all rows with consumers', () => { expect(screen.getByText('groupId1')).toBeInTheDocument(); @@ -33,13 +46,64 @@ describe('List', () => { await waitFor(() => { userEvent.type( screen.getByPlaceholderText('Search by Consumer Group ID'), - 'groupId1' + consumerGroupMock[0].groupId ); }); - expect(screen.getByText('groupId1')).toBeInTheDocument(); - expect(screen.getByText('groupId2')).toBeInTheDocument(); + 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', () => { + it('should test the sort order functionality', async () => { + const thElement = screen.getByText(/consumer group id/i); + expect(thElement).toBeInTheDocument(); + expect(thElement).toHaveStyle(`color:${theme.table.th.color.active}`); }); }); }); + + describe('consumerGroups are fetched with custom parameters', () => { + it('should test the order by functionality of another element', async () => { + const sortOrder = jest.fn(); + setUpComponent({ + consumerGroups: consumerGroupMock, + setConsumerGroupsSortOrderBy: sortOrder, + }); + const thElement = screen.getByText(/num of members/i); + expect(thElement).toBeInTheDocument(); + + userEvent.click(thElement); + expect(sortOrder).toBeCalled(); + }); + + it('should view the ordered list with the right prop', () => { + setUpComponent({ + consumerGroups: consumerGroupMock, + orderBy: ConsumerGroupOrdering.MEMBERS, + }); + expect(screen.getByText(/num of members/i)).toHaveStyle( + `color:${theme.table.th.color.active}` + ); + }); + }); }); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/__test__/ConsumerGroups.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/__test__/ConsumerGroups.spec.tsx index cbb90a7a2f..59e8e4fd3d 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/__test__/ConsumerGroups.spec.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/__test__/ConsumerGroups.spec.tsx @@ -10,6 +10,7 @@ import { consumerGroups } from 'redux/reducers/consumerGroups/__test__/fixtures' import { render } from 'lib/testHelpers'; import fetchMock from 'fetch-mock'; import { Route } from 'react-router'; +import { ConsumerGroupOrdering, SortOrder } from 'generated-sources'; const clusterName = 'cluster1'; @@ -24,42 +25,41 @@ const renderComponent = () => ); describe('ConsumerGroup', () => { - afterEach(() => { - fetchMock.reset(); - }); - it('renders with initial state', async () => { renderComponent(); expect(screen.getByRole('progressbar')).toBeInTheDocument(); }); - it('renders with 404 from consumer groups', async () => { - const consumerGroupsMock = fetchMock.getOnce( - `/api/clusters/${clusterName}/consumer-groups`, - 404 - ); + describe('Fetching Mock', () => { + const url = `/api/clusters/${clusterName}/consumer-groups/paged?orderBy=${ConsumerGroupOrdering.NAME}&sortOrder=${SortOrder.ASC}`; + afterEach(() => { + fetchMock.reset(); + }); + it('renders with 404 from consumer groups', async () => { + const consumerGroupsMock = fetchMock.getOnce(url, 404); - renderComponent(); + renderComponent(); - await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy()); + await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy()); - expect(screen.queryByText('Consumers')).not.toBeInTheDocument(); - expect(screen.queryByRole('table')).not.toBeInTheDocument(); - }); + expect(screen.queryByText('Consumers')).not.toBeInTheDocument(); + expect(screen.queryByRole('table')).not.toBeInTheDocument(); + }); - it('renders with 200 from consumer groups', async () => { - const consumerGroupsMock = fetchMock.getOnce( - `/api/clusters/${clusterName}/consumer-groups`, - consumerGroups - ); + it('renders with 200 from consumer groups', async () => { + const consumerGroupsMock = fetchMock.getOnce(url, { + pagedCount: 1, + consumerGroups, + }); - renderComponent(); + renderComponent(); - await waitForElementToBeRemoved(() => screen.getByRole('progressbar')); - await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy()); + await waitForElementToBeRemoved(() => screen.getByRole('progressbar')); + await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy()); - expect(screen.getByText('Consumers')).toBeInTheDocument(); - expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getByText('Consumers')).toBeInTheDocument(); + expect(screen.getByRole('table')).toBeInTheDocument(); + }); }); }); diff --git a/kafka-ui-react-app/src/components/common/SmartTable/SmartTable.tsx b/kafka-ui-react-app/src/components/common/SmartTable/SmartTable.tsx index 301ee671a3..0b04279dbf 100644 --- a/kafka-ui-react-app/src/components/common/SmartTable/SmartTable.tsx +++ b/kafka-ui-react-app/src/components/common/SmartTable/SmartTable.tsx @@ -69,19 +69,25 @@ export const SmartTable = ({ /> ); }); + let checkboxElement = null; + + if (selectable) { + checkboxElement = allSelectable ? ( + + ) : ( + + ); + } + return ( - {allSelectable ? ( - - ) : ( - - )} + {checkboxElement} {headerCells} ); diff --git a/kafka-ui-react-app/src/components/common/SmartTable/TableColumn.tsx b/kafka-ui-react-app/src/components/common/SmartTable/TableColumn.tsx index 971aacf7bd..96d7280547 100644 --- a/kafka-ui-react-app/src/components/common/SmartTable/TableColumn.tsx +++ b/kafka-ui-react-app/src/components/common/SmartTable/TableColumn.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { TableState } from 'lib/hooks/useTableState'; import { SortOrder } from 'generated-sources'; +import * as S from 'components/common/table/TableHeaderCell/TableHeaderCell.styled'; +import { DefaultTheme, StyledComponent } from 'styled-components'; export interface OrderableProps { orderBy: OT | null; @@ -77,7 +79,12 @@ export const SelectCell: React.FC = ({ onChange(e.target.checked); }; - const El = el; + let El: 'td' | StyledComponent<'th', DefaultTheme>; + if (el === 'th') { + El = S.TableHeaderCell; + } else { + El = el; + } return ( diff --git a/kafka-ui-react-app/src/components/common/table/Table/TableKeyLink.styled.ts b/kafka-ui-react-app/src/components/common/table/Table/TableKeyLink.styled.ts index c3a0aa3d50..3b5fdf5953 100644 --- a/kafka-ui-react-app/src/components/common/table/Table/TableKeyLink.styled.ts +++ b/kafka-ui-react-app/src/components/common/table/Table/TableKeyLink.styled.ts @@ -1,9 +1,19 @@ -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; -export const TableKeyLink = styled.td` - & > a { - color: ${({ theme }) => theme.table.link.color}; +const tableLinkMixin = css( + ({ theme }) => ` + & > a { + color: ${theme.table.link.color}; font-weight: 500; text-overflow: ellipsis; } +` +); + +export const TableKeyLink = styled.td` + ${tableLinkMixin} +`; + +export const SmartTableKeyLink = styled.div` + ${tableLinkMixin} `; diff --git a/kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.styled.ts b/kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.styled.ts index 74c98764cb..44ad528457 100644 --- a/kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.styled.ts +++ b/kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.styled.ts @@ -1,8 +1,10 @@ import styled, { css } from 'styled-components'; +import { SortOrder } from 'generated-sources'; -interface TitleProps { +export interface TitleProps { isOrderable?: boolean; isOrdered?: boolean; + sortOrder?: SortOrder; } const orderableMixin = css( @@ -45,20 +47,28 @@ const orderableMixin = css( ` ); -const orderedMixin = css( +const ASCMixin = css( ({ theme: { table } }) => ` - color: ${table.th.color.active}; - &::before { + color: ${table.th.color.active}; + + &:before { border-bottom-color: ${table.th.color.active}; - } - &::after { + } + ` +); + +const DESCMixin = css( + ({ theme: { table } }) => ` + color: ${table.th.color.active}; + + &:after { border-top-color: ${table.th.color.active}; - } + } ` ); export const Title = styled.span( - ({ isOrderable, isOrdered, theme: { table } }) => css` + ({ isOrderable, isOrdered, sortOrder, theme: { table } }) => css` font-family: Inter, sans-serif; font-size: 12px; font-style: normal; @@ -75,7 +85,9 @@ export const Title = styled.span( ${isOrderable && orderableMixin} - ${isOrderable && isOrdered && orderedMixin} + ${isOrderable && isOrdered && sortOrder === SortOrder.ASC && ASCMixin} + + ${isOrderable && isOrdered && sortOrder === SortOrder.DESC && DESCMixin} ` ); diff --git a/kafka-ui-react-app/src/components/common/table/TableHeaderCell/__test__/TableHeaderCell.styled.spec.tsx b/kafka-ui-react-app/src/components/common/table/TableHeaderCell/__test__/TableHeaderCell.styled.spec.tsx new file mode 100644 index 0000000000..e780c044ce --- /dev/null +++ b/kafka-ui-react-app/src/components/common/table/TableHeaderCell/__test__/TableHeaderCell.styled.spec.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { render } from 'lib/testHelpers'; +import * as S from 'components/common/table/TableHeaderCell/TableHeaderCell.styled'; +import { SortOrder } from 'generated-sources'; +import { screen } from '@testing-library/react'; +import theme from 'theme/theme'; + +describe('TableHeaderCell.Styled', () => { + describe('Title Component', () => { + const DEFAULT_TITLE_TEXT = 'Text'; + const setUpComponent = ( + props: Partial = {}, + text: string = DEFAULT_TITLE_TEXT + ) => { + render( + + {text || DEFAULT_TITLE_TEXT} + + ); + }; + describe('test the default Parameters', () => { + beforeEach(() => { + setUpComponent(); + }); + it('should test the props of Title Component', () => { + const titleElement = screen.getByText(DEFAULT_TITLE_TEXT); + expect(titleElement).toBeInTheDocument(); + expect(titleElement).toHaveStyle( + `color: ${theme.table.th.color.active};` + ); + expect(titleElement).toHaveStyleRule( + 'border-bottom-color', + theme.table.th.color.active, + { + modifier: '&:before', + } + ); + }); + }); + + describe('Custom props', () => { + it('should test the sort order styling of Title Component', () => { + setUpComponent({ + sortOrder: SortOrder.DESC, + }); + + const titleElement = screen.getByText(DEFAULT_TITLE_TEXT); + expect(titleElement).toBeInTheDocument(); + expect(titleElement).toHaveStyleRule( + 'color', + theme.table.th.color.active + ); + expect(titleElement).toHaveStyleRule( + 'border-top-color', + theme.table.th.color.active, + { + modifier: '&:after', + } + ); + }); + + it('should test the Title Component styling without the ordering', () => { + setUpComponent({ + isOrderable: false, + isOrdered: false, + }); + + const titleElement = screen.getByText(DEFAULT_TITLE_TEXT); + expect(titleElement).toHaveStyleRule('cursor', 'default'); + }); + }); + }); + + describe('Preview Component', () => { + const DEFAULT_TEXT = 'DEFAULT_TEXT'; + it('should render the preview and check themes values', () => { + render({DEFAULT_TEXT}); + const element = screen.getByText(DEFAULT_TEXT); + expect(element).toBeInTheDocument(); + expect(element).toHaveStyleRule( + 'background', + theme.table.th.backgroundColor.normal + ); + expect(element).toHaveStyleRule( + 'color', + theme.table.th.previewColor.normal + ); + }); + }); +}); diff --git a/kafka-ui-react-app/src/redux/reducers/consumerGroups/__test__/consumerGroupSlice.spec.ts b/kafka-ui-react-app/src/redux/reducers/consumerGroups/__test__/consumerGroupSlice.spec.ts new file mode 100644 index 0000000000..2bf20606ad --- /dev/null +++ b/kafka-ui-react-app/src/redux/reducers/consumerGroups/__test__/consumerGroupSlice.spec.ts @@ -0,0 +1,49 @@ +import { store } from 'redux/store'; +import { + sortBy, + getConsumerGroupsOrderBy, + getConsumerGroupsSortOrder, + getAreConsumerGroupsPagedFulfilled, + fetchConsumerGroupsPaged, + selectAll, +} from 'redux/reducers/consumerGroups/consumerGroupsSlice'; +import { ConsumerGroupOrdering, SortOrder } from 'generated-sources'; +import { consumerGroups } from 'redux/reducers/consumerGroups/__test__/fixtures'; + +describe('Consumer Groups Slice', () => { + describe('Actions', () => { + it('should test the sortBy actions', () => { + expect(store.getState().consumerGroups.sortOrder).toBe(SortOrder.ASC); + + store.dispatch(sortBy(ConsumerGroupOrdering.STATE)); + expect(getConsumerGroupsOrderBy(store.getState())).toBe( + ConsumerGroupOrdering.STATE + ); + expect(getConsumerGroupsSortOrder(store.getState())).toBe(SortOrder.DESC); + store.dispatch(sortBy(ConsumerGroupOrdering.STATE)); + expect(getConsumerGroupsSortOrder(store.getState())).toBe(SortOrder.ASC); + }); + }); + + describe('Thunk Actions', () => { + it('should check the fetchConsumerPaged ', () => { + store.dispatch({ + type: fetchConsumerGroupsPaged.fulfilled.type, + payload: { + consumerGroups, + }, + }); + + expect(getAreConsumerGroupsPagedFulfilled(store.getState())).toBeTruthy(); + expect(selectAll(store.getState())).toEqual(consumerGroups); + + store.dispatch({ + type: fetchConsumerGroupsPaged.fulfilled.type, + payload: { + consumerGroups: null, + }, + }); + expect(selectAll(store.getState())).toEqual([]); + }); + }); +}); diff --git a/kafka-ui-react-app/src/redux/reducers/consumerGroups/__test__/fixtures.ts b/kafka-ui-react-app/src/redux/reducers/consumerGroups/__test__/fixtures.ts index 7c130ef472..237291df20 100644 --- a/kafka-ui-react-app/src/redux/reducers/consumerGroups/__test__/fixtures.ts +++ b/kafka-ui-react-app/src/redux/reducers/consumerGroups/__test__/fixtures.ts @@ -25,6 +25,11 @@ export const consumerGroups = [ }, ]; +export const consumerGroupsPage = { + totalPages: 1, + consumerGroups, +}; + export const consumerGroupPayload = { groupId: 'amazon.msk.canary.group.broker-1', members: 0, diff --git a/kafka-ui-react-app/src/redux/reducers/consumerGroups/consumerGroupsSlice.ts b/kafka-ui-react-app/src/redux/reducers/consumerGroups/consumerGroupsSlice.ts index d381917a22..e37943cf59 100644 --- a/kafka-ui-react-app/src/redux/reducers/consumerGroups/consumerGroupsSlice.ts +++ b/kafka-ui-react-app/src/redux/reducers/consumerGroups/consumerGroupsSlice.ts @@ -3,12 +3,15 @@ import { createEntityAdapter, createSlice, createSelector, + PayloadAction, } from '@reduxjs/toolkit'; import { Configuration, - ConsumerGroup, ConsumerGroupDetails, + ConsumerGroupOrdering, ConsumerGroupsApi, + ConsumerGroupsPageResponse, + SortOrder, } from 'generated-sources'; import { BASE_PARAMS } from 'lib/constants'; import { getResponse } from 'lib/errorHandling'; @@ -19,20 +22,28 @@ import { RootState, } from 'redux/interfaces'; import { createFetchingSelector } from 'redux/reducers/loader/selectors'; +import { EntityState } from '@reduxjs/toolkit/src/entities/models'; const apiClientConf = new Configuration(BASE_PARAMS); export const api = new ConsumerGroupsApi(apiClientConf); -export const fetchConsumerGroups = createAsyncThunk< - ConsumerGroup[], - ClusterName +export const fetchConsumerGroupsPaged = createAsyncThunk< + ConsumerGroupsPageResponse, + { + clusterName: ClusterName; + orderBy?: ConsumerGroupOrdering; + sortOrder?: SortOrder; + } >( - 'consumerGroups/fetchConsumerGroups', - async (clusterName: ClusterName, { rejectWithValue }) => { + 'consumerGroups/fetchConsumerGroupsPaged', + async ({ clusterName, orderBy, sortOrder }, { rejectWithValue }) => { try { - return await api.getConsumerGroups({ + const response = await api.getConsumerGroupsPageRaw({ clusterName, + orderBy, + sortOrder, }); + return await response.value(); } catch (error) { return rejectWithValue(await getResponse(error as Response)); } @@ -105,19 +116,45 @@ export const resetConsumerGroupOffsets = createAsyncThunk< } } ); +const SCHEMAS_PAGE_COUNT = 1; const consumerGroupsAdapter = createEntityAdapter({ selectId: (consumerGroup) => consumerGroup.groupId, }); -const consumerGroupsSlice = createSlice({ +interface ConsumerGroupState extends EntityState { + orderBy: ConsumerGroupOrdering | null; + sortOrder: SortOrder; + totalPages: number; +} + +const initialState: ConsumerGroupState = { + orderBy: ConsumerGroupOrdering.NAME, + sortOrder: SortOrder.ASC, + totalPages: SCHEMAS_PAGE_COUNT, + ...consumerGroupsAdapter.getInitialState(), +}; + +export const consumerGroupsSlice = createSlice({ name: 'consumerGroups', - initialState: consumerGroupsAdapter.getInitialState(), - reducers: {}, + initialState, + reducers: { + sortBy: (state, action: PayloadAction) => { + state.orderBy = action.payload; + state.sortOrder = + state.orderBy === action.payload && state.sortOrder === SortOrder.ASC + ? SortOrder.DESC + : SortOrder.ASC; + }, + }, extraReducers: (builder) => { - builder.addCase(fetchConsumerGroups.fulfilled, (state, { payload }) => { - consumerGroupsAdapter.setAll(state, payload); - }); + builder.addCase( + fetchConsumerGroupsPaged.fulfilled, + (state, { payload }) => { + state.totalPages = payload.pageCount || SCHEMAS_PAGE_COUNT; + consumerGroupsAdapter.setAll(state, payload.consumerGroups || []); + } + ); builder.addCase(fetchConsumerGroupDetails.fulfilled, (state, { payload }) => consumerGroupsAdapter.upsertOne(state, payload) ); @@ -127,13 +164,17 @@ const consumerGroupsSlice = createSlice({ }, }); -export const { selectAll, selectById } = - consumerGroupsAdapter.getSelectors( - ({ consumerGroups }) => consumerGroups - ); +export const { sortBy } = consumerGroupsSlice.actions; -export const getAreConsumerGroupsFulfilled = createSelector( - createFetchingSelector('consumerGroups/fetchConsumerGroups'), +const consumerGroupsState = ({ + consumerGroups, +}: RootState): ConsumerGroupState => consumerGroups; + +export const { selectAll, selectById } = + consumerGroupsAdapter.getSelectors(consumerGroupsState); + +export const getAreConsumerGroupsPagedFulfilled = createSelector( + createFetchingSelector('consumerGroups/fetchConsumerGroupsPaged'), (status) => status === 'fulfilled' ); @@ -152,4 +193,19 @@ export const getIsOffsetReseted = createSelector( (status) => status === 'fulfilled' ); +export const getConsumerGroupsOrderBy = createSelector( + consumerGroupsState, + (state) => state.orderBy +); + +export const getConsumerGroupsSortOrder = createSelector( + consumerGroupsState, + (state) => state.sortOrder +); + +export const getConsumerGroupsTotalPages = createSelector( + consumerGroupsState, + (state) => state.totalPages +); + export default consumerGroupsSlice.reducer;