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
This commit is contained in:
Mgrdich 2022-03-24 13:44:15 +04:00 committed by GitHub
parent 8908d6839c
commit 73266f86af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 588 additions and 127 deletions

View file

@ -3,22 +3,32 @@ import { ClusterName } from 'redux/interfaces';
import { Switch, useParams } from 'react-router-dom'; import { Switch, useParams } from 'react-router-dom';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
import Details from 'components/ConsumerGroups/Details/Details'; 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 ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets';
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
import { import {
fetchConsumerGroups, fetchConsumerGroupsPaged,
getAreConsumerGroupsFulfilled, getAreConsumerGroupsPagedFulfilled,
getConsumerGroupsOrderBy,
getConsumerGroupsSortOrder,
} from 'redux/reducers/consumerGroups/consumerGroupsSlice'; } 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(); const dispatch = useAppDispatch();
const { clusterName } = useParams<{ clusterName: ClusterName }>(); const { clusterName } = useParams<{ clusterName: ClusterName }>();
const isFetched = useAppSelector(getAreConsumerGroupsFulfilled); const isFetched = useAppSelector(getAreConsumerGroupsPagedFulfilled);
const orderBy = useAppSelector(getConsumerGroupsOrderBy);
const sortOrder = useAppSelector(getConsumerGroupsSortOrder);
React.useEffect(() => { React.useEffect(() => {
dispatch(fetchConsumerGroups(clusterName)); dispatch(
}, [clusterName, dispatch]); fetchConsumerGroupsPaged({
clusterName,
orderBy: orderBy || undefined,
sortOrder,
})
);
}, [clusterName, orderBy, sortOrder, dispatch]);
if (isFetched) { if (isFetched) {
return ( return (
@ -26,7 +36,7 @@ const ConsumerGroups: React.FC = () => {
<BreadcrumbRoute <BreadcrumbRoute
exact exact
path="/ui/clusters/:clusterName/consumer-groups" path="/ui/clusters/:clusterName/consumer-groups"
component={List} component={ListContainer}
/> />
<BreadcrumbRoute <BreadcrumbRoute
exact exact

View file

@ -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>
);
};

View file

@ -1,18 +1,61 @@
import React from 'react'; import React, { useMemo } from 'react';
import { Table } from 'components/common/table/Table/Table.styled';
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
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';
import { useAppSelector } from 'lib/hooks/redux'; import {
import { selectAll } from 'redux/reducers/consumerGroups/consumerGroupsSlice'; 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 List: React.FC<Props> = ({
const consumerGroups = useAppSelector(selectAll); consumerGroups,
sortOrder,
orderBy,
totalPages,
setConsumerGroupsSortOrderBy,
}) => {
const [searchText, setSearchText] = React.useState<string>(''); 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) => { const handleInputChange = (search: string) => {
setSearchText(search); setSearchText(search);
}; };
@ -27,36 +70,31 @@ const List: React.FC = () => {
handleSearch={handleInputChange} handleSearch={handleInputChange}
/> />
</ControlPanelWrapper> </ControlPanelWrapper>
<Table isFullwidth> <SmartTable
<thead> tableState={tableState}
<tr> isFullwidth
<TableHeaderCell title="Consumer Group ID" /> placeholder="No active consumer groups"
<TableHeaderCell title="Num Of Members" /> hoverable
<TableHeaderCell title="Num Of Topics" /> >
<TableHeaderCell title="Messages Behind" /> <TableColumn
<TableHeaderCell title="Coordinator" /> title="Consumer Group ID"
<TableHeaderCell title="State" /> cell={GroupIDCell}
</tr> orderValue={ConsumerGroupOrdering.NAME}
</thead>
<tbody>
{consumerGroups
.filter(
(consumerGroup) =>
!searchText || consumerGroup?.groupId?.indexOf(searchText) >= 0
)
.map((consumerGroup) => (
<ListItem
key={consumerGroup.groupId}
consumerGroup={consumerGroup}
/> />
))} <TableColumn
{consumerGroups.length === 0 && ( title="Num Of Members"
<tr> field="members"
<td colSpan={10}>No active consumer groups</td> orderValue={ConsumerGroupOrdering.MEMBERS}
</tr> />
)} <TableColumn title="Num Of Topics" field="topics" />
</tbody> <TableColumn title="Messages Behind" field="messagesBehind" />
</Table> <TableColumn title="Coordinator" field="coordinator.id" />
<TableColumn
title="State"
cell={StatusCell}
orderValue={ConsumerGroupOrdering.STATE}
/>
</SmartTable>
</div> </div>
); );
}; };

View file

@ -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);

View file

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

View file

@ -1,27 +1,40 @@
import React from 'react'; 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 { screen, waitFor } 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 { store } from 'redux/store'; import { consumerGroups as consumerGroupMock } from 'redux/reducers/consumerGroups/__test__/fixtures';
import { fetchConsumerGroups } from 'redux/reducers/consumerGroups/consumerGroupsSlice'; import { ConsumerGroupOrdering, SortOrder } from 'generated-sources';
import { consumerGroups } from 'redux/reducers/consumerGroups/__test__/fixtures'; import theme from 'theme/theme';
describe('List', () => { 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', () => { it('renders empty table', () => {
setUpComponent();
expect(screen.getByRole('table')).toBeInTheDocument(); expect(screen.getByRole('table')).toBeInTheDocument();
expect(screen.getByText('No active consumer groups')).toBeInTheDocument(); expect(screen.getByText('No active consumer groups')).toBeInTheDocument();
}); });
describe('consumerGroups are fetched', () => { describe('consumerGroups are fetched', () => {
beforeEach(() => { beforeEach(() => setUpComponent({ consumerGroups: consumerGroupMock }));
store.dispatch({
type: fetchConsumerGroups.fulfilled.type,
payload: consumerGroups,
});
});
it('renders all rows with consumers', () => { it('renders all rows with consumers', () => {
expect(screen.getByText('groupId1')).toBeInTheDocument(); expect(screen.getByText('groupId1')).toBeInTheDocument();
@ -33,13 +46,64 @@ describe('List', () => {
await waitFor(() => { await waitFor(() => {
userEvent.type( userEvent.type(
screen.getByPlaceholderText('Search by Consumer Group ID'), screen.getByPlaceholderText('Search by Consumer Group ID'),
'groupId1' consumerGroupMock[0].groupId
); );
}); });
expect(screen.getByText('groupId1')).toBeInTheDocument(); expect(
expect(screen.getByText('groupId2')).toBeInTheDocument(); 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}`
);
});
});
}); });

View file

@ -10,6 +10,7 @@ import { consumerGroups } 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 } from 'react-router';
import { ConsumerGroupOrdering, SortOrder } from 'generated-sources';
const clusterName = 'cluster1'; const clusterName = 'cluster1';
@ -24,21 +25,19 @@ const renderComponent = () =>
); );
describe('ConsumerGroup', () => { describe('ConsumerGroup', () => {
afterEach(() => {
fetchMock.reset();
});
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', () => {
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 () => { it('renders with 404 from consumer groups', async () => {
const consumerGroupsMock = fetchMock.getOnce( const consumerGroupsMock = fetchMock.getOnce(url, 404);
`/api/clusters/${clusterName}/consumer-groups`,
404
);
renderComponent(); renderComponent();
@ -49,10 +48,10 @@ describe('ConsumerGroup', () => {
}); });
it('renders with 200 from consumer groups', async () => { it('renders with 200 from consumer groups', async () => {
const consumerGroupsMock = fetchMock.getOnce( const consumerGroupsMock = fetchMock.getOnce(url, {
`/api/clusters/${clusterName}/consumer-groups`, pagedCount: 1,
consumerGroups consumerGroups,
); });
renderComponent(); renderComponent();
@ -63,3 +62,4 @@ describe('ConsumerGroup', () => {
expect(screen.getByRole('table')).toBeInTheDocument(); expect(screen.getByRole('table')).toBeInTheDocument();
}); });
}); });
});

View file

@ -69,9 +69,10 @@ export const SmartTable = <T, TId extends IdType, OT = never>({
/> />
); );
}); });
return ( let checkboxElement = null;
<tr>
{allSelectable ? ( if (selectable) {
checkboxElement = allSelectable ? (
<SelectCell <SelectCell
rowIndex={-1} rowIndex={-1}
el="th" el="th"
@ -81,7 +82,12 @@ export const SmartTable = <T, TId extends IdType, OT = never>({
/> />
) : ( ) : (
<S.TableHeaderCell /> <S.TableHeaderCell />
)} );
}
return (
<tr>
{checkboxElement}
{headerCells} {headerCells}
</tr> </tr>
); );

View file

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import { TableState } from 'lib/hooks/useTableState'; import { TableState } from 'lib/hooks/useTableState';
import { SortOrder } from 'generated-sources'; 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> { export interface OrderableProps<OT> {
orderBy: OT | null; orderBy: OT | null;
@ -77,7 +79,12 @@ export const SelectCell: React.FC<SelectCellProps> = ({
onChange(e.target.checked); onChange(e.target.checked);
}; };
const El = el; let El: 'td' | StyledComponent<'th', DefaultTheme>;
if (el === 'th') {
El = S.TableHeaderCell;
} else {
El = el;
}
return ( return (
<El> <El>

View file

@ -1,9 +1,19 @@
import styled from 'styled-components'; import styled, { css } from 'styled-components';
export const TableKeyLink = styled.td` const tableLinkMixin = css(
({ theme }) => `
& > a { & > a {
color: ${({ theme }) => theme.table.link.color}; color: ${theme.table.link.color};
font-weight: 500; font-weight: 500;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
`
);
export const TableKeyLink = styled.td`
${tableLinkMixin}
`;
export const SmartTableKeyLink = styled.div`
${tableLinkMixin}
`; `;

View file

@ -1,8 +1,10 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { SortOrder } from 'generated-sources';
interface TitleProps { export interface TitleProps {
isOrderable?: boolean; isOrderable?: boolean;
isOrdered?: boolean; isOrdered?: boolean;
sortOrder?: SortOrder;
} }
const orderableMixin = css( const orderableMixin = css(
@ -45,20 +47,28 @@ const orderableMixin = css(
` `
); );
const orderedMixin = css( const ASCMixin = css(
({ theme: { table } }) => ` ({ theme: { table } }) => `
color: ${table.th.color.active}; color: ${table.th.color.active};
&::before {
&:before {
border-bottom-color: ${table.th.color.active}; 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}; border-top-color: ${table.th.color.active};
} }
` `
); );
export const Title = styled.span<TitleProps>( export const Title = styled.span<TitleProps>(
({ isOrderable, isOrdered, theme: { table } }) => css` ({ isOrderable, isOrdered, sortOrder, theme: { table } }) => css`
font-family: Inter, sans-serif; font-family: Inter, sans-serif;
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@ -75,7 +85,9 @@ export const Title = styled.span<TitleProps>(
${isOrderable && orderableMixin} ${isOrderable && orderableMixin}
${isOrderable && isOrdered && orderedMixin} ${isOrderable && isOrdered && sortOrder === SortOrder.ASC && ASCMixin}
${isOrderable && isOrdered && sortOrder === SortOrder.DESC && DESCMixin}
` `
); );

View file

@ -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
);
});
});
});

View file

@ -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([]);
});
});
});

View file

@ -25,6 +25,11 @@ export const consumerGroups = [
}, },
]; ];
export const consumerGroupsPage = {
totalPages: 1,
consumerGroups,
};
export const consumerGroupPayload = { export const consumerGroupPayload = {
groupId: 'amazon.msk.canary.group.broker-1', groupId: 'amazon.msk.canary.group.broker-1',
members: 0, members: 0,

View file

@ -3,12 +3,15 @@ import {
createEntityAdapter, createEntityAdapter,
createSlice, createSlice,
createSelector, createSelector,
PayloadAction,
} from '@reduxjs/toolkit'; } from '@reduxjs/toolkit';
import { import {
Configuration, Configuration,
ConsumerGroup,
ConsumerGroupDetails, ConsumerGroupDetails,
ConsumerGroupOrdering,
ConsumerGroupsApi, ConsumerGroupsApi,
ConsumerGroupsPageResponse,
SortOrder,
} from 'generated-sources'; } from 'generated-sources';
import { BASE_PARAMS } from 'lib/constants'; import { BASE_PARAMS } from 'lib/constants';
import { getResponse } from 'lib/errorHandling'; import { getResponse } from 'lib/errorHandling';
@ -19,20 +22,28 @@ import {
RootState, RootState,
} from 'redux/interfaces'; } from 'redux/interfaces';
import { createFetchingSelector } from 'redux/reducers/loader/selectors'; import { createFetchingSelector } from 'redux/reducers/loader/selectors';
import { EntityState } from '@reduxjs/toolkit/src/entities/models';
const apiClientConf = new Configuration(BASE_PARAMS); const apiClientConf = new Configuration(BASE_PARAMS);
export const api = new ConsumerGroupsApi(apiClientConf); export const api = new ConsumerGroupsApi(apiClientConf);
export const fetchConsumerGroups = createAsyncThunk< export const fetchConsumerGroupsPaged = createAsyncThunk<
ConsumerGroup[], ConsumerGroupsPageResponse,
ClusterName {
clusterName: ClusterName;
orderBy?: ConsumerGroupOrdering;
sortOrder?: SortOrder;
}
>( >(
'consumerGroups/fetchConsumerGroups', 'consumerGroups/fetchConsumerGroupsPaged',
async (clusterName: ClusterName, { rejectWithValue }) => { async ({ clusterName, orderBy, sortOrder }, { rejectWithValue }) => {
try { try {
return await api.getConsumerGroups({ const response = await api.getConsumerGroupsPageRaw({
clusterName, clusterName,
orderBy,
sortOrder,
}); });
return await response.value();
} catch (error) { } catch (error) {
return rejectWithValue(await getResponse(error as Response)); return rejectWithValue(await getResponse(error as Response));
} }
@ -105,19 +116,45 @@ export const resetConsumerGroupOffsets = createAsyncThunk<
} }
} }
); );
const SCHEMAS_PAGE_COUNT = 1;
const consumerGroupsAdapter = createEntityAdapter<ConsumerGroupDetails>({ const consumerGroupsAdapter = createEntityAdapter<ConsumerGroupDetails>({
selectId: (consumerGroup) => consumerGroup.groupId, 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', name: 'consumerGroups',
initialState: consumerGroupsAdapter.getInitialState(), initialState,
reducers: {}, 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) => { extraReducers: (builder) => {
builder.addCase(fetchConsumerGroups.fulfilled, (state, { payload }) => { builder.addCase(
consumerGroupsAdapter.setAll(state, payload); fetchConsumerGroupsPaged.fulfilled,
}); (state, { payload }) => {
state.totalPages = payload.pageCount || SCHEMAS_PAGE_COUNT;
consumerGroupsAdapter.setAll(state, payload.consumerGroups || []);
}
);
builder.addCase(fetchConsumerGroupDetails.fulfilled, (state, { payload }) => builder.addCase(fetchConsumerGroupDetails.fulfilled, (state, { payload }) =>
consumerGroupsAdapter.upsertOne(state, payload) consumerGroupsAdapter.upsertOne(state, payload)
); );
@ -127,13 +164,17 @@ const consumerGroupsSlice = createSlice({
}, },
}); });
export const { selectAll, selectById } = export const { sortBy } = consumerGroupsSlice.actions;
consumerGroupsAdapter.getSelectors<RootState>(
({ consumerGroups }) => consumerGroups
);
export const getAreConsumerGroupsFulfilled = createSelector( const consumerGroupsState = ({
createFetchingSelector('consumerGroups/fetchConsumerGroups'), consumerGroups,
}: RootState): ConsumerGroupState => consumerGroups;
export const { selectAll, selectById } =
consumerGroupsAdapter.getSelectors<RootState>(consumerGroupsState);
export const getAreConsumerGroupsPagedFulfilled = createSelector(
createFetchingSelector('consumerGroups/fetchConsumerGroupsPaged'),
(status) => status === 'fulfilled' (status) => status === 'fulfilled'
); );
@ -152,4 +193,19 @@ export const getIsOffsetReseted = createSelector(
(status) => status === 'fulfilled' (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; export default consumerGroupsSlice.reducer;