浏览代码

Issues/1525 add sort order consumer group page (#1747)

* Adding Consumer Groups Paged Reducer and action creator and get Selector logic

* Adding fetchConsumerGroupsPaged to the ConsumerGroups Paged

* Code refactoring and adding general fixtures For Consumer Group List

* Adding Container Redux Connect structure to the List of Consumer Groups

* Adding Selectors and action creators for orderBy and Sort Order

* Adding All Necessary components to render the smart Tables in Consumer Groups

* Adding SmartTable to the Consumer Groups page + fixing allSelectable Checkbox bug in the SmartTable Component

* Primitive Tests for ConsumerGroupsTableCells to test rendering and mockup the component and table data

* Finalizing Tests for consumer Groups table , minor code bug fix in the Consumer Groups table structure

* Adding Order By to the Consumer Groups Table , View part

* Adding order By to the Consumer Groups pages

* Adding order By to the Consumer Groups pages with SortBy functionality

* Code refactor in the ConsumerGroups component and its related tests

* Code refactor in the ConsumerGroups component and its related tests

* adding Tests in the Consumer Groups List

* Fixing the Sorting styling Bug in the Table order

* Adding additional Tests to the ConsumerGroups List tests

* Adding additional Tests for TableHeaderCell styled component

* Deleting obsolete codes from the consumer Groups Slice + minor table header test type fix

* Adding Tests for the consumerGroupSlice

* Adding Tests for the consumerGroupSlice

* Consumer Groups table minor code modifications

* Minor Code bug fixes in the SmartTable Component
Mgrdich 3 年之前
父节点
当前提交
73266f86af

+ 17 - 7
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 = () => {
         <BreadcrumbRoute
           exact
           path="/ui/clusters/:clusterName/consumer-groups"
-          component={List}
+          component={ListContainer}
         />
         <BreadcrumbRoute
           exact

+ 23 - 0
kafka-ui-react-app/src/components/ConsumerGroups/List/ConsumerGroupsTableCells.tsx

@@ -0,0 +1,23 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { Tag } from 'components/common/Tag/Tag.styled';
+import { TableCellProps } from 'components/common/SmartTable/TableColumn';
+import getTagColor from 'components/ConsumerGroups/Utils/TagColor';
+import { ConsumerGroup } from 'generated-sources';
+import { SmartTableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
+
+export const StatusCell: React.FC<TableCellProps<ConsumerGroup, string>> = ({
+  dataItem,
+}) => {
+  return <Tag color={getTagColor(dataItem)}>{dataItem.state}</Tag>;
+};
+
+export const GroupIDCell: React.FC<TableCellProps<ConsumerGroup, string>> = ({
+  dataItem: { groupId },
+}) => {
+  return (
+    <SmartTableKeyLink>
+      <Link to={`consumer-groups/${groupId}`}>{groupId}</Link>
+    </SmartTableKeyLink>
+  );
+};

+ 76 - 38
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<Props> = ({
+  consumerGroups,
+  sortOrder,
+  orderBy,
+  totalPages,
+  setConsumerGroupsSortOrderBy,
+}) => {
   const [searchText, setSearchText] = React.useState<string>('');
 
+  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}
         />
       </ControlPanelWrapper>
-      <Table isFullwidth>
-        <thead>
-          <tr>
-            <TableHeaderCell title="Consumer Group ID" />
-            <TableHeaderCell title="Num Of Members" />
-            <TableHeaderCell title="Num Of Topics" />
-            <TableHeaderCell title="Messages Behind" />
-            <TableHeaderCell title="Coordinator" />
-            <TableHeaderCell title="State" />
-          </tr>
-        </thead>
-        <tbody>
-          {consumerGroups
-            .filter(
-              (consumerGroup) =>
-                !searchText || consumerGroup?.groupId?.indexOf(searchText) >= 0
-            )
-            .map((consumerGroup) => (
-              <ListItem
-                key={consumerGroup.groupId}
-                consumerGroup={consumerGroup}
-              />
-            ))}
-          {consumerGroups.length === 0 && (
-            <tr>
-              <td colSpan={10}>No active consumer groups</td>
-            </tr>
-          )}
-        </tbody>
-      </Table>
+      <SmartTable
+        tableState={tableState}
+        isFullwidth
+        placeholder="No active consumer groups"
+        hoverable
+      >
+        <TableColumn
+          title="Consumer Group ID"
+          cell={GroupIDCell}
+          orderValue={ConsumerGroupOrdering.NAME}
+        />
+        <TableColumn
+          title="Num Of Members"
+          field="members"
+          orderValue={ConsumerGroupOrdering.MEMBERS}
+        />
+        <TableColumn title="Num Of Topics" field="topics" />
+        <TableColumn title="Messages Behind" field="messagesBehind" />
+        <TableColumn title="Coordinator" field="coordinator.id" />
+        <TableColumn
+          title="State"
+          cell={StatusCell}
+          orderValue={ConsumerGroupOrdering.STATE}
+        />
+      </SmartTable>
     </div>
   );
 };

+ 23 - 0
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);

+ 64 - 0
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<ConsumerGroup, string, never> = {
+    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(
+        <GroupIDCell
+          rowIndex={1}
+          dataItem={consumerGroup}
+          tableState={mockTableState}
+        />
+      );
+      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(
+        <StatusCell
+          rowIndex={1}
+          dataItem={consumerGroup}
+          tableState={mockTableState}
+        />
+      );
+      expect(
+        screen.getByText(consumerGroup.state as string)
+      ).toBeInTheDocument();
+    });
+  });
+});

+ 78 - 14
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(<List />, { store }));
+  const setUpComponent = (props: Partial<Props> = {}) => {
+    const {
+      consumerGroups,
+      orderBy,
+      sortOrder,
+      totalPages,
+      setConsumerGroupsSortOrderBy,
+    } = props;
+    return render(
+      <List
+        consumerGroups={consumerGroups || []}
+        orderBy={orderBy || ConsumerGroupOrdering.NAME}
+        sortOrder={sortOrder || SortOrder.ASC}
+        setConsumerGroupsSortOrderBy={setConsumerGroupsSortOrderBy || jest.fn()}
+        totalPages={totalPages || 1}
+      />
+    );
+  };
 
   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}`
+      );
     });
   });
 });

+ 24 - 24
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();
+    });
   });
 });

+ 17 - 11
kafka-ui-react-app/src/components/common/SmartTable/SmartTable.tsx

@@ -69,19 +69,25 @@ export const SmartTable = <T, TId extends IdType, OT = never>({
         />
       );
     });
+    let checkboxElement = null;
+
+    if (selectable) {
+      checkboxElement = allSelectable ? (
+        <SelectCell
+          rowIndex={-1}
+          el="th"
+          selectable
+          selected={tableState.selectedCount === tableState.data.length}
+          onChange={tableState.toggleSelection}
+        />
+      ) : (
+        <S.TableHeaderCell />
+      );
+    }
+
     return (
       <tr>
-        {allSelectable ? (
-          <SelectCell
-            rowIndex={-1}
-            el="th"
-            selectable
-            selected={tableState.selectedCount === tableState.data.length}
-            onChange={tableState.toggleSelection}
-          />
-        ) : (
-          <S.TableHeaderCell />
-        )}
+        {checkboxElement}
         {headerCells}
       </tr>
     );

+ 8 - 1
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<OT> {
   orderBy: OT | null;
@@ -77,7 +79,12 @@ export const SelectCell: React.FC<SelectCellProps> = ({
     onChange(e.target.checked);
   };
 
-  const El = el;
+  let El: 'td' | StyledComponent<'th', DefaultTheme>;
+  if (el === 'th') {
+    El = S.TableHeaderCell;
+  } else {
+    El = el;
+  }
 
   return (
     <El>

+ 14 - 4
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}
 `;

+ 21 - 9
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<TitleProps>(
-  ({ 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<TitleProps>(
 
     ${isOrderable && orderableMixin}
 
-    ${isOrderable && isOrdered && orderedMixin}
+    ${isOrderable && isOrdered && sortOrder === SortOrder.ASC && ASCMixin}
+
+    ${isOrderable && isOrdered && sortOrder === SortOrder.DESC && DESCMixin}
   `
 );
 

+ 94 - 0
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<S.TitleProps> = {},
+      text: string = DEFAULT_TITLE_TEXT
+    ) => {
+      render(
+        <S.Title
+          isOrderable={'isOrderable' in props ? props.isOrderable : true}
+          isOrdered={'isOrdered' in props ? props.isOrdered : true}
+          sortOrder={props.sortOrder || SortOrder.ASC}
+        >
+          {text || DEFAULT_TITLE_TEXT}
+        </S.Title>
+      );
+    };
+    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(<S.Preview>{DEFAULT_TEXT}</S.Preview>);
+      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
+      );
+    });
+  });
+});

+ 49 - 0
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([]);
+    });
+  });
+});

+ 5 - 0
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,

+ 74 - 18
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<ConsumerGroupDetails>({
   selectId: (consumerGroup) => consumerGroup.groupId,
 });
 
-const consumerGroupsSlice = createSlice({
+interface ConsumerGroupState extends EntityState<ConsumerGroupDetails> {
+  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<ConsumerGroupOrdering>) => {
+      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 { sortBy } = consumerGroupsSlice.actions;
+
+const consumerGroupsState = ({
+  consumerGroups,
+}: RootState): ConsumerGroupState => consumerGroups;
+
 export const { selectAll, selectById } =
-  consumerGroupsAdapter.getSelectors<RootState>(
-    ({ consumerGroups }) => consumerGroups
-  );
+  consumerGroupsAdapter.getSelectors<RootState>(consumerGroupsState);
 
-export const getAreConsumerGroupsFulfilled = createSelector(
-  createFetchingSelector('consumerGroups/fetchConsumerGroups'),
+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;