From 19cfdb07f24f0be8d57f986d9936f65b9a5e9ea9 Mon Sep 17 00:00:00 2001 From: Oleg Shuralev Date: Tue, 16 Mar 2021 16:36:13 +0300 Subject: [PATCH] [CHORE] Pagination. Refactor topic reducer --- kafka-ui-react-app/package-lock.json | 19 +- kafka-ui-react-app/package.json | 4 +- .../src/components/Schemas/List/List.tsx | 6 +- .../__test__/__snapshots__/New.spec.tsx.snap | 34 +-- .../src/components/Topics/List/List.tsx | 10 +- .../components/Topics/List/ListContainer.ts | 2 + .../Topics/List/__tests__/List.spec.tsx | 8 +- .../common/Breadcrumb/Breadcrumb.tsx | 8 +- .../Breadcrumb/__tests__/Breadcrumb.spec.tsx | 6 +- .../common/Pagination/PageControl.tsx | 25 ++ .../common/Pagination/Pagination.tsx | 130 ++++++++ .../Pagination/__tests__/PageControl.spec.tsx | 34 +++ .../Pagination/__tests__/Pagination.spec.tsx | 96 ++++++ .../__snapshots__/PageControl.spec.tsx.snap | 13 + .../__snapshots__/Pagination.spec.tsx.snap | 288 ++++++++++++++++++ kafka-ui-react-app/src/lib/constants.ts | 2 + .../src/lib/hooks/usePagination.ts | 4 +- .../src/redux/actions/actions.ts | 8 +- .../src/redux/actions/thunks/topics.ts | 74 ++++- .../src/redux/reducers/topics/reducer.ts | 41 +-- .../src/redux/reducers/topics/selectors.ts | 2 + 21 files changed, 707 insertions(+), 107 deletions(-) create mode 100644 kafka-ui-react-app/src/components/common/Pagination/PageControl.tsx create mode 100644 kafka-ui-react-app/src/components/common/Pagination/Pagination.tsx create mode 100644 kafka-ui-react-app/src/components/common/Pagination/__tests__/PageControl.spec.tsx create mode 100644 kafka-ui-react-app/src/components/common/Pagination/__tests__/Pagination.spec.tsx create mode 100644 kafka-ui-react-app/src/components/common/Pagination/__tests__/__snapshots__/PageControl.spec.tsx.snap create mode 100644 kafka-ui-react-app/src/components/common/Pagination/__tests__/__snapshots__/Pagination.spec.tsx.snap diff --git a/kafka-ui-react-app/package-lock.json b/kafka-ui-react-app/package-lock.json index 288e3e8f69..e2d1ee71d0 100644 --- a/kafka-ui-react-app/package-lock.json +++ b/kafka-ui-react-app/package-lock.json @@ -19335,12 +19335,11 @@ "dev": true }, "ts-jest": { - "version": "26.5.1", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.1.tgz", - "integrity": "sha512-G7Rmo3OJMvlqE79amJX8VJKDiRcd7/r61wh9fnvvG8cAjhA9edklGw/dCxRSQmfZ/z8NDums5srSVgwZos1qfg==", + "version": "26.5.3", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.3.tgz", + "integrity": "sha512-nBiiFGNvtujdLryU7MiMQh1iPmnZ/QvOskBbD2kURiI1MwqvxlxNnaAB/z9TbslMqCsSbu5BXvSSQPc5tvHGeA==", "dev": true, "requires": { - "@types/jest": "26.x", "bs-logger": "0.x", "buffer-from": "1.x", "fast-json-stable-stringify": "2.x", @@ -19494,9 +19493,9 @@ "integrity": "sha512-bna6Yi1pRznoo6Bz1cE6btB/Yy8Xywytyfrzu/wc+NFW3ZF0I+2iCGImhBsoYYCOWuICtRO4yHcnDlzgo1AdNg==" }, "typescript": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz", - "integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", + "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", "dev": true }, "unicode-canonical-property-names-ecmascript": { @@ -21622,9 +21621,9 @@ } }, "yargs-parser": { - "version": "20.2.5", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.5.tgz", - "integrity": "sha512-jYRGS3zWy20NtDtK2kBgo/TlAoy5YUuhD9/LZ7z7W4j1Fdw2cqD0xEEclf8fxc8xjD6X5Qr+qQQwCEsP8iRiYg==", + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", "dev": true }, "yn": { diff --git a/kafka-ui-react-app/package.json b/kafka-ui-react-app/package.json index 66d7fb27e9..c28c56a8ab 100644 --- a/kafka-ui-react-app/package.json +++ b/kafka-ui-react-app/package.json @@ -112,9 +112,9 @@ "prettier": "^2.2.1", "react-scripts": "4.0.2", "redux-mock-store": "^1.5.4", - "ts-jest": "^26.5.1", + "ts-jest": "^26.5.3", "ts-node": "^9.1.1", - "typescript": "^4.1.5" + "typescript": "^4.2.3" }, "engines": { "node": ">=14.15.4" diff --git a/kafka-ui-react-app/src/components/Schemas/List/List.tsx b/kafka-ui-react-app/src/components/Schemas/List/List.tsx index 5e8ab6681b..bd77910e2d 100644 --- a/kafka-ui-react-app/src/components/Schemas/List/List.tsx +++ b/kafka-ui-react-app/src/components/Schemas/List/List.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { SchemaSubject } from 'generated-sources'; -import { NavLink, useParams } from 'react-router-dom'; +import { Link, useParams } from 'react-router-dom'; import { clusterSchemaNewPath } from 'lib/paths'; import { ClusterName } from 'redux/interfaces'; import PageLoader from 'components/common/PageLoader/PageLoader'; @@ -33,12 +33,12 @@ const List: React.FC = ({
{!isReadOnly && (
- Create Schema - +
)}
diff --git a/kafka-ui-react-app/src/components/Schemas/New/__test__/__snapshots__/New.spec.tsx.snap b/kafka-ui-react-app/src/components/Schemas/New/__test__/__snapshots__/New.spec.tsx.snap index ad4acb4a5c..03848230ec 100644 --- a/kafka-ui-react-app/src/components/Schemas/New/__test__/__snapshots__/New.spec.tsx.snap +++ b/kafka-ui-react-app/src/components/Schemas/New/__test__/__snapshots__/New.spec.tsx.snap @@ -60,35 +60,21 @@ exports[`New View matches snapshot 1`] = `
  • - - - - - Schema Registry - - - - + Schema Registry + + +
  • = ({ areTopicsFetching, topics, externalTopics, + totalPages, fetchTopicsList, }) => { const { isReadOnly } = React.useContext(ClusterContext); @@ -58,12 +61,12 @@ const List: React.FC = ({
    {!isReadOnly && ( - Add a Topic - + )}
    @@ -93,6 +96,7 @@ const List: React.FC = ({ )} + )} diff --git a/kafka-ui-react-app/src/components/Topics/List/ListContainer.ts b/kafka-ui-react-app/src/components/Topics/List/ListContainer.ts index 4e54baa125..6ade3cf75a 100644 --- a/kafka-ui-react-app/src/components/Topics/List/ListContainer.ts +++ b/kafka-ui-react-app/src/components/Topics/List/ListContainer.ts @@ -5,6 +5,7 @@ import { getTopicList, getExternalTopicList, getAreTopicsFetching, + getTopicListTotalPages, } from 'redux/reducers/topics/selectors'; import List from './List'; @@ -12,6 +13,7 @@ const mapStateToProps = (state: RootState) => ({ areTopicsFetching: getAreTopicsFetching(state), topics: getTopicList(state), externalTopics: getExternalTopicList(state), + totalPages: getTopicListTotalPages(state), }); const mapDispatchToProps = { diff --git a/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx b/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx index 3d8f185b58..bd065658d6 100644 --- a/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx @@ -1,5 +1,5 @@ -import { mount } from 'enzyme'; import React from 'react'; +import { mount } from 'enzyme'; import { StaticRouter } from 'react-router-dom'; import ClusterContext from 'components/contexts/ClusterContext'; import List from '../List'; @@ -14,12 +14,13 @@ describe('List', () => { areTopicsFetching={false} topics={[]} externalTopics={[]} + totalPages={1} fetchTopicsList={jest.fn()} /> ); - expect(component.exists('NavLink')).toBeFalsy(); + expect(component.exists('Link')).toBeFalsy(); }); }); @@ -32,12 +33,13 @@ describe('List', () => { areTopicsFetching={false} topics={[]} externalTopics={[]} + totalPages={1} fetchTopicsList={jest.fn()} /> ); - expect(component.exists('NavLink')).toBeTruthy(); + expect(component.exists('Link')).toBeTruthy(); }); }); }); diff --git a/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.tsx b/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.tsx index 2e93e2a685..7181bf7dfe 100644 --- a/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.tsx +++ b/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import { NavLink } from 'react-router-dom'; +import { Link } from 'react-router-dom'; -export interface Link { +export interface BreadcrumbItem { label: string; href: string; } interface Props { - links?: Link[]; + links?: BreadcrumbItem[]; } const Breadcrumb: React.FC = ({ links, children }) => { @@ -17,7 +17,7 @@ const Breadcrumb: React.FC = ({ links, children }) => { {links && links.map(({ label, href }) => (
  • - {label} + {label}
  • ))} diff --git a/kafka-ui-react-app/src/components/common/Breadcrumb/__tests__/Breadcrumb.spec.tsx b/kafka-ui-react-app/src/components/common/Breadcrumb/__tests__/Breadcrumb.spec.tsx index c8b3a58156..c07c943ae8 100644 --- a/kafka-ui-react-app/src/components/common/Breadcrumb/__tests__/Breadcrumb.spec.tsx +++ b/kafka-ui-react-app/src/components/common/Breadcrumb/__tests__/Breadcrumb.spec.tsx @@ -1,10 +1,10 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { StaticRouter } from 'react-router-dom'; -import Breadcrumb, { Link } from '../Breadcrumb'; +import Breadcrumb, { BreadcrumbItem } from '../Breadcrumb'; describe('Breadcrumb component', () => { - const links: Link[] = [ + const links: BreadcrumbItem[] = [ { label: 'link1', href: 'link1href', @@ -28,7 +28,7 @@ describe('Breadcrumb component', () => { ); it('renders the list of links', () => { - component.find(`NavLink`).forEach((link, idx) => { + component.find(`Link`).forEach((link, idx) => { expect(link.prop('to')).toEqual(links[idx].href); expect(link.contains(links[idx].label)).toBeTruthy(); }); diff --git a/kafka-ui-react-app/src/components/common/Pagination/PageControl.tsx b/kafka-ui-react-app/src/components/common/Pagination/PageControl.tsx new file mode 100644 index 0000000000..a67898f4bd --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Pagination/PageControl.tsx @@ -0,0 +1,25 @@ +import cx from 'classnames'; +import React from 'react'; +import { Link } from 'react-router-dom'; + +export interface PageControlProps { + current: boolean; + url: string; + page: number; +} + +const PageControl: React.FC = ({ current, url, page }) => { + const classNames = cx('pagination-link', { + 'is-current': current, + }); + + return ( +
  • + + {page} + +
  • + ); +}; + +export default PageControl; diff --git a/kafka-ui-react-app/src/components/common/Pagination/Pagination.tsx b/kafka-ui-react-app/src/components/common/Pagination/Pagination.tsx new file mode 100644 index 0000000000..e2d80c4af8 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Pagination/Pagination.tsx @@ -0,0 +1,130 @@ +import { PER_PAGE } from 'lib/constants'; +import usePagination from 'lib/hooks/usePagination'; +import { range } from 'lodash'; +import React from 'react'; +import { Link } from 'react-router-dom'; +import PageControl from './PageControl'; + +export interface PaginationProps { + totalPages: number; +} + +const NEIGHBOURS = 2; + +const Pagination: React.FC = ({ totalPages }) => { + const { page, perPage, pathname } = usePagination(); + + const currentPage = page || 1; + const currentPerPage = perPage || PER_PAGE; + + const getPath = (newPage: number) => + `${pathname}?page=${Math.max(newPage, 1)}&perPage=${currentPerPage}`; + + const pages = React.useMemo(() => { + // Total visible numbers: neighbours, current, first & last + const totalNumbers = NEIGHBOURS * 2 + 3; + // totalNumbers + `...`*2 + const totalBlocks = totalNumbers + 2; + + if (totalPages <= totalBlocks) { + return range(1, totalPages + 1); + } + + const startPage = Math.max( + 2, + Math.min(currentPage - NEIGHBOURS, totalPages) + ); + const endPage = Math.min( + totalPages - 1, + Math.min(currentPage + NEIGHBOURS, totalPages) + ); + + let p = range(startPage, endPage + 1); + + const hasLeftSpill = startPage > 2; + const hasRightSpill = totalPages - endPage > 1; + const spillOffset = totalNumbers - (p.length + 1); + + switch (true) { + case hasLeftSpill && !hasRightSpill: { + p = [...range(startPage - spillOffset - 1, startPage - 1), ...p]; + break; + } + + case !hasLeftSpill && hasRightSpill: { + p = [...p, ...range(endPage + 1, endPage + spillOffset + 1)]; + break; + } + + default: + break; + } + + return p; + }, []); + + return ( + + ); +}; + +export default Pagination; diff --git a/kafka-ui-react-app/src/components/common/Pagination/__tests__/PageControl.spec.tsx b/kafka-ui-react-app/src/components/common/Pagination/__tests__/PageControl.spec.tsx new file mode 100644 index 0000000000..997f901958 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Pagination/__tests__/PageControl.spec.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { StaticRouter } from 'react-router'; +import PageControl, { PageControlProps } from '../PageControl'; + +const page = 138; + +describe('PageControl', () => { + const setupWrapper = (props: Partial = {}) => ( + + + + ); + + it('renders current page', () => { + const wrapper = mount(setupWrapper({ current: true })); + expect(wrapper.exists('.pagination-link.is-current')).toBeTruthy(); + }); + + it('renders non-current page', () => { + const wrapper = mount(setupWrapper({ current: false })); + expect(wrapper.exists('.pagination-link.is-current')).toBeFalsy(); + }); + + it('renders page number', () => { + const wrapper = mount(setupWrapper({ current: false })); + expect(wrapper.text()).toEqual(String(page)); + }); + + it('matches snapshot', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/kafka-ui-react-app/src/components/common/Pagination/__tests__/Pagination.spec.tsx b/kafka-ui-react-app/src/components/common/Pagination/__tests__/Pagination.spec.tsx new file mode 100644 index 0000000000..9411c7fc83 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Pagination/__tests__/Pagination.spec.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { StaticRouter } from 'react-router'; +import Pagination, { PaginationProps } from '../Pagination'; + +describe('Pagination', () => { + const setupWrapper = (search = '', props: Partial = {}) => ( + + + + ); + + describe('next & prev buttons', () => { + it('renders disable prev button and enabled next link', () => { + const wrapper = mount(setupWrapper('?page=1')); + expect(wrapper.exists('a.pagination-previous')).toBeFalsy(); + expect( + wrapper.find('button.pagination-previous').instance() + ).toBeDisabled(); + expect(wrapper.exists('a.pagination-next')).toBeTruthy(); + }); + + it('renders disable next button and enabled prev link', () => { + const wrapper = mount(setupWrapper('?page=11')); + expect(wrapper.exists('a.pagination-previous')).toBeTruthy(); + expect(wrapper.exists('button.pagination-next')).toBeTruthy(); + }); + + it('renders next & prev links with correct path', () => { + const wrapper = mount(setupWrapper('?page=5&perPage=20')); + expect(wrapper.exists('a.pagination-previous')).toBeTruthy(); + expect(wrapper.find('a.pagination-previous').prop('href')).toEqual( + '/my/test/path/23?page=4&perPage=20' + ); + expect(wrapper.exists('a.pagination-next')).toBeTruthy(); + expect(wrapper.find('a.pagination-next').prop('href')).toEqual( + '/my/test/path/23?page=6&perPage=20' + ); + }); + }); + + describe('spread', () => { + it('renders 1 spread element after first page control', () => { + const wrapper = mount(setupWrapper('?page=8')); + expect(wrapper.find('span.pagination-ellipsis').length).toEqual(1); + expect(wrapper.find('ul li').at(1).text()).toEqual('…'); + }); + + it('renders 1 spread element before last spread control', () => { + const wrapper = mount(setupWrapper('?page=2')); + expect(wrapper.find('span.pagination-ellipsis').length).toEqual(1); + expect(wrapper.find('ul li').at(7).text()).toEqual('…'); + }); + + it('renders 2 spread elements', () => { + const wrapper = mount(setupWrapper('?page=6')); + expect(wrapper.find('span.pagination-ellipsis').length).toEqual(2); + expect(wrapper.find('ul li').at(0).text()).toEqual('1'); + expect(wrapper.find('ul li').at(1).text()).toEqual('…'); + expect(wrapper.find('ul li').at(7).text()).toEqual('…'); + expect(wrapper.find('ul li').at(8).text()).toEqual('11'); + }); + + it('renders 0 spread elements', () => { + const wrapper = mount(setupWrapper('?page=2', { totalPages: 8 })); + expect(wrapper.find('span.pagination-ellipsis').length).toEqual(0); + expect(wrapper.find('ul li').length).toEqual(8); + }); + }); + + describe('current page', () => { + it('adds is-current class to correct page if page param is set', () => { + const wrapper = mount(setupWrapper('?page=8')); + expect(wrapper.exists('a.pagination-link.is-current')).toBeTruthy(); + expect(wrapper.find('a.pagination-link.is-current').text()).toEqual('8'); + }); + + it('adds is-current class to correct page even if page param is not set', () => { + const wrapper = mount(setupWrapper('', { totalPages: 8 })); + expect(wrapper.exists('a.pagination-link.is-current')).toBeTruthy(); + expect(wrapper.find('a.pagination-link.is-current').text()).toEqual('1'); + }); + + it('adds no is-current class if page numder is invalid', () => { + const wrapper = mount(setupWrapper('?page=80')); + expect(wrapper.exists('a.pagination-link.is-current')).toBeFalsy(); + }); + }); + + it('matches snapshot', () => { + const wrapper = mount(setupWrapper()); + expect(wrapper.find('Pagination')).toMatchSnapshot(); + }); +}); + +// span.pagination-ellipsis diff --git a/kafka-ui-react-app/src/components/common/Pagination/__tests__/__snapshots__/PageControl.spec.tsx.snap b/kafka-ui-react-app/src/components/common/Pagination/__tests__/__snapshots__/PageControl.spec.tsx.snap new file mode 100644 index 0000000000..25ecfe9aa1 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Pagination/__tests__/__snapshots__/PageControl.spec.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PageControl matches snapshot 1`] = ` +
  • + + 138 + +
  • +`; diff --git a/kafka-ui-react-app/src/components/common/Pagination/__tests__/__snapshots__/Pagination.spec.tsx.snap b/kafka-ui-react-app/src/components/common/Pagination/__tests__/__snapshots__/Pagination.spec.tsx.snap new file mode 100644 index 0000000000..a07b49160d --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Pagination/__tests__/__snapshots__/Pagination.spec.tsx.snap @@ -0,0 +1,288 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pagination matches snapshot 1`] = ` + + + +`; diff --git a/kafka-ui-react-app/src/lib/constants.ts b/kafka-ui-react-app/src/lib/constants.ts index f69f390ec9..53bf47144e 100644 --- a/kafka-ui-react-app/src/lib/constants.ts +++ b/kafka-ui-react-app/src/lib/constants.ts @@ -16,3 +16,5 @@ export const MILLISECONDS_IN_DAY = 86_400_000; export const MILLISECONDS_IN_SECOND = 1_000; export const BYTES_IN_GB = 1_073_741_824; + +export const PER_PAGE = 25; diff --git a/kafka-ui-react-app/src/lib/hooks/usePagination.ts b/kafka-ui-react-app/src/lib/hooks/usePagination.ts index 1351742609..41e3e1b113 100644 --- a/kafka-ui-react-app/src/lib/hooks/usePagination.ts +++ b/kafka-ui-react-app/src/lib/hooks/usePagination.ts @@ -1,7 +1,8 @@ import { useLocation } from 'react-router'; const usePagination = () => { - const params = new URLSearchParams(useLocation().search); + const { search, pathname } = useLocation(); + const params = new URLSearchParams(search); const page = params.get('page'); const perPage = params.get('perPage'); @@ -9,6 +10,7 @@ const usePagination = () => { return { page: page ? Number(page) : undefined, perPage: perPage ? Number(perPage) : undefined, + pathname, }; }; diff --git a/kafka-ui-react-app/src/redux/actions/actions.ts b/kafka-ui-react-app/src/redux/actions/actions.ts index 6cc0ad8845..dce0fc1adc 100644 --- a/kafka-ui-react-app/src/redux/actions/actions.ts +++ b/kafka-ui-react-app/src/redux/actions/actions.ts @@ -62,25 +62,25 @@ export const fetchTopicDetailsAction = createAsyncAction( 'GET_TOPIC_DETAILS__REQUEST', 'GET_TOPIC_DETAILS__SUCCESS', 'GET_TOPIC_DETAILS__FAILURE' -)(); +)(); export const fetchTopicConfigAction = createAsyncAction( 'GET_TOPIC_CONFIG__REQUEST', 'GET_TOPIC_CONFIG__SUCCESS', 'GET_TOPIC_CONFIG__FAILURE' -)(); +)(); export const createTopicAction = createAsyncAction( 'POST_TOPIC__REQUEST', 'POST_TOPIC__SUCCESS', 'POST_TOPIC__FAILURE' -)(); +)(); export const updateTopicAction = createAsyncAction( 'PATCH_TOPIC__REQUEST', 'PATCH_TOPIC__SUCCESS', 'PATCH_TOPIC__FAILURE' -)(); +)(); export const fetchConsumerGroupsAction = createAsyncAction( 'GET_CONSUMER_GROUPS__REQUEST', diff --git a/kafka-ui-react-app/src/redux/actions/thunks/topics.ts b/kafka-ui-react-app/src/redux/actions/thunks/topics.ts index 8bbc56dbeb..da5616b174 100644 --- a/kafka-ui-react-app/src/redux/actions/thunks/topics.ts +++ b/kafka-ui-react-app/src/redux/actions/thunks/topics.ts @@ -16,7 +16,6 @@ import { TopicFormDataRaw, TopicsState, } from 'redux/interfaces'; - import { BASE_PARAMS } from 'lib/constants'; import * as actions from '../actions'; @@ -82,19 +81,25 @@ export const fetchTopicMessages = ( export const fetchTopicDetails = ( clusterName: ClusterName, topicName: TopicName -): PromiseThunkResult => async (dispatch) => { +): PromiseThunkResult => async (dispatch, getState) => { dispatch(actions.fetchTopicDetailsAction.request()); try { const topicDetails = await topicsApiClient.getTopicDetails({ clusterName, topicName, }); - dispatch( - actions.fetchTopicDetailsAction.success({ - topicName, - details: topicDetails, - }) - ); + const state = getState().topics; + const newState = { + ...state, + byName: { + ...state.byName, + [topicName]: { + ...state.byName[topicName], + ...topicDetails, + }, + }, + }; + dispatch(actions.fetchTopicDetailsAction.success(newState)); } catch (e) { dispatch(actions.fetchTopicDetailsAction.failure()); } @@ -103,14 +108,29 @@ export const fetchTopicDetails = ( export const fetchTopicConfig = ( clusterName: ClusterName, topicName: TopicName -): PromiseThunkResult => async (dispatch) => { +): PromiseThunkResult => async (dispatch, getState) => { dispatch(actions.fetchTopicConfigAction.request()); try { const config = await topicsApiClient.getTopicConfigs({ clusterName, topicName, }); - dispatch(actions.fetchTopicConfigAction.success({ topicName, config })); + + const state = getState().topics; + const newState = { + ...state, + byName: { + ...state.byName, + [topicName]: { + ...state.byName[topicName], + config: config.map((inputConfig) => ({ + ...inputConfig, + })), + }, + }, + }; + + dispatch(actions.fetchTopicConfigAction.success(newState)); } catch (e) { dispatch(actions.fetchTopicConfigAction.failure()); } @@ -155,14 +175,27 @@ const formatTopicFormData = (form: TopicFormDataRaw): TopicFormData => { export const createTopic = ( clusterName: ClusterName, form: TopicFormDataRaw -): PromiseThunkResult => async (dispatch) => { +): PromiseThunkResult => async (dispatch, getState) => { dispatch(actions.createTopicAction.request()); try { const topic: Topic = await topicsApiClient.createTopic({ clusterName, topicFormData: formatTopicFormData(form), }); - dispatch(actions.createTopicAction.success(topic)); + + const state = getState().topics; + const newState = { + ...state, + byName: { + ...state.byName, + [topic.name]: { + ...topic, + }, + }, + allNames: [...state.allNames, topic.name], + }; + + dispatch(actions.createTopicAction.success(newState)); } catch (e) { dispatch(actions.createTopicAction.failure()); } @@ -171,7 +204,7 @@ export const createTopic = ( export const updateTopic = ( clusterName: ClusterName, form: TopicFormDataRaw -): PromiseThunkResult => async (dispatch) => { +): PromiseThunkResult => async (dispatch, getState) => { dispatch(actions.updateTopicAction.request()); try { const topic: Topic = await topicsApiClient.updateTopic({ @@ -179,7 +212,20 @@ export const updateTopic = ( topicName: form.name, topicFormData: formatTopicFormData(form), }); - dispatch(actions.updateTopicAction.success(topic)); + + const state = getState().topics; + const newState = { + ...state, + byName: { + ...state.byName, + [topic.name]: { + ...state.byName[topic.name], + ...topic, + }, + }, + }; + + dispatch(actions.updateTopicAction.success(newState)); } catch (e) { dispatch(actions.updateTopicAction.failure()); } diff --git a/kafka-ui-react-app/src/redux/reducers/topics/reducer.ts b/kafka-ui-react-app/src/redux/reducers/topics/reducer.ts index bba97d142a..665bb57e31 100644 --- a/kafka-ui-react-app/src/redux/reducers/topics/reducer.ts +++ b/kafka-ui-react-app/src/redux/reducers/topics/reducer.ts @@ -1,4 +1,4 @@ -import { Topic, TopicMessage } from 'generated-sources'; +import { TopicMessage } from 'generated-sources'; import { Action, TopicsState } from 'redux/interfaces'; import { getType } from 'typesafe-actions'; import * as actions from 'redux/actions'; @@ -10,15 +10,6 @@ export const initialState: TopicsState = { messages: [], }; -const addToTopicList = (state: TopicsState, payload: Topic): TopicsState => { - const newState: TopicsState = { - ...state, - }; - newState.allNames.push(payload.name); - newState.byName[payload.name] = { ...payload }; - return newState; -}; - const transformTopicMessages = ( state: TopicsState, messages: TopicMessage[] @@ -47,35 +38,13 @@ const transformTopicMessages = ( const reducer = (state = initialState, action: Action): TopicsState => { switch (action.type) { case getType(actions.fetchTopicsListAction.success): - return action.payload; case getType(actions.fetchTopicDetailsAction.success): - return { - ...state, - byName: { - ...state.byName, - [action.payload.topicName]: { - ...state.byName[action.payload.topicName], - ...action.payload.details, - }, - }, - }; + case getType(actions.fetchTopicConfigAction.success): + case getType(actions.createTopicAction.success): + case getType(actions.updateTopicAction.success): + return action.payload; case getType(actions.fetchTopicMessagesAction.success): return transformTopicMessages(state, action.payload); - case getType(actions.fetchTopicConfigAction.success): - return { - ...state, - byName: { - ...state.byName, - [action.payload.topicName]: { - ...state.byName[action.payload.topicName], - config: action.payload.config.map((inputConfig) => ({ - ...inputConfig, - })), - }, - }, - }; - case getType(actions.createTopicAction.success): - return addToTopicList(state, action.payload); default: return state; } diff --git a/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts b/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts index 5271ecd4e6..1da0cf9110 100644 --- a/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts +++ b/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts @@ -14,6 +14,8 @@ const getAllNames = (state: RootState) => topicsState(state).allNames; const getTopicMap = (state: RootState) => topicsState(state).byName; export const getTopicMessages = (state: RootState) => topicsState(state).messages; +export const getTopicListTotalPages = (state: RootState) => + topicsState(state).totalPages; const getTopicListFetchingStatus = createFetchingSelector('GET_TOPICS'); const getTopicDetailsFetchingStatus = createFetchingSelector(