#188 Pagination. Refactor topic reducer (#255)

This commit is contained in:
Oleg Shur 2021-03-17 12:55:17 +03:00 committed by GitHub
parent bbdd60b7a5
commit 49a7564366
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 707 additions and 107 deletions

View file

@ -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": {

View file

@ -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"

View file

@ -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<ListProps> = ({
<div className="level">
{!isReadOnly && (
<div className="level-item level-right">
<NavLink
<Link
className="button is-primary"
to={clusterSchemaNewPath(clusterName)}
>
Create Schema
</NavLink>
</Link>
</div>
)}
</div>

View file

@ -60,35 +60,21 @@ exports[`New View matches snapshot 1`] = `
<li
key="/ui/clusters/undefined/schemas"
>
<NavLink
<Link
to="/ui/clusters/undefined/schemas"
>
<Link
aria-current={null}
to={
Object {
"hash": "",
"pathname": "/ui/clusters/undefined/schemas",
"search": "",
"state": null,
}
}
<LinkAnchor
href="/ui/clusters/undefined/schemas"
navigate={[Function]}
>
<LinkAnchor
aria-current={null}
<a
href="/ui/clusters/undefined/schemas"
navigate={[Function]}
onClick={[Function]}
>
<a
aria-current={null}
href="/ui/clusters/undefined/schemas"
onClick={[Function]}
>
Schema Registry
</a>
</LinkAnchor>
</Link>
</NavLink>
Schema Registry
</a>
</LinkAnchor>
</Link>
</li>
<li
className="is-active"

View file

@ -1,18 +1,20 @@
import React from 'react';
import { TopicWithDetailedInfo, ClusterName } from 'redux/interfaces';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import { NavLink, useParams } from 'react-router-dom';
import { Link, useParams } from 'react-router-dom';
import { clusterTopicNewPath } from 'lib/paths';
import usePagination from 'lib/hooks/usePagination';
import { FetchTopicsListParams } from 'redux/actions';
import ClusterContext from 'components/contexts/ClusterContext';
import PageLoader from 'components/common/PageLoader/PageLoader';
import Pagination from 'components/common/Pagination/Pagination';
import ListItem from './ListItem';
interface Props {
areTopicsFetching: boolean;
topics: TopicWithDetailedInfo[];
externalTopics: TopicWithDetailedInfo[];
totalPages: number;
fetchTopicsList(props: FetchTopicsListParams): void;
}
@ -20,6 +22,7 @@ const List: React.FC<Props> = ({
areTopicsFetching,
topics,
externalTopics,
totalPages,
fetchTopicsList,
}) => {
const { isReadOnly } = React.useContext(ClusterContext);
@ -58,12 +61,12 @@ const List: React.FC<Props> = ({
</div>
<div className="level-item level-right">
{!isReadOnly && (
<NavLink
<Link
className="button is-primary"
to={clusterTopicNewPath(clusterName)}
>
Add a Topic
</NavLink>
</Link>
)}
</div>
</div>
@ -93,6 +96,7 @@ const List: React.FC<Props> = ({
)}
</tbody>
</table>
<Pagination totalPages={totalPages} />
</div>
)}
</div>

View file

@ -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 = {

View file

@ -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()}
/>
</ClusterContext.Provider>
</StaticRouter>
);
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()}
/>
</ClusterContext.Provider>
</StaticRouter>
);
expect(component.exists('NavLink')).toBeTruthy();
expect(component.exists('Link')).toBeTruthy();
});
});
});

View file

@ -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<Props> = ({ links, children }) => {
@ -17,7 +17,7 @@ const Breadcrumb: React.FC<Props> = ({ links, children }) => {
{links &&
links.map(({ label, href }) => (
<li key={href}>
<NavLink to={href}>{label}</NavLink>
<Link to={href}>{label}</Link>
</li>
))}

View file

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

View file

@ -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<PageControlProps> = ({ current, url, page }) => {
const classNames = cx('pagination-link', {
'is-current': current,
});
return (
<li>
<Link className={classNames} to={url} aria-label={`Goto page ${page}`}>
{page}
</Link>
</li>
);
};
export default PageControl;

View file

@ -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<PaginationProps> = ({ 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 (
<nav
className="pagination is-small is-right"
role="navigation"
aria-label="pagination"
>
{currentPage > 1 ? (
<Link className="pagination-previous" to={getPath(currentPage - 1)}>
Previous
</Link>
) : (
<button type="button" className="pagination-previous" disabled>
Previous
</button>
)}
{currentPage < totalPages ? (
<Link className="pagination-next" to={getPath(currentPage + 1)}>
Next page
</Link>
) : (
<button type="button" className="pagination-next" disabled>
Next page
</button>
)}
{totalPages > 1 && (
<ul className="pagination-list">
{!pages.includes(1) && (
<PageControl
page={1}
current={currentPage === 1}
url={getPath(1)}
/>
)}
{!pages.includes(2) && (
<li>
<span className="pagination-ellipsis">&hellip;</span>
</li>
)}
{pages.map((p) => (
<PageControl
key={`page-${p}`}
page={p}
current={p === currentPage}
url={getPath(p)}
/>
))}
{!pages.includes(totalPages - 1) && (
<li>
<span className="pagination-ellipsis">&hellip;</span>
</li>
)}
{!pages.includes(totalPages) && (
<PageControl
page={totalPages}
current={currentPage === totalPages}
url={getPath(totalPages)}
/>
)}
</ul>
)}
</nav>
);
};
export default Pagination;

View file

@ -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<PageControlProps> = {}) => (
<StaticRouter>
<PageControl url="/test" page={page} current {...props} />
</StaticRouter>
);
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(<PageControl url="/test" page={page} current />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -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<PaginationProps> = {}) => (
<StaticRouter location={{ pathname: '/my/test/path/23', search }}>
<Pagination totalPages={11} {...props} />
</StaticRouter>
);
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

View file

@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PageControl matches snapshot 1`] = `
<li>
<Link
aria-label="Goto page 138"
className="pagination-link is-current"
to="/test"
>
138
</Link>
</li>
`;

View file

@ -0,0 +1,288 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Pagination matches snapshot 1`] = `
<Pagination
totalPages={11}
>
<nav
aria-label="pagination"
className="pagination is-small is-right"
role="navigation"
>
<button
className="pagination-previous"
disabled={true}
type="button"
>
Previous
</button>
<Link
className="pagination-next"
to="/my/test/path/23?page=2&perPage=25"
>
<LinkAnchor
className="pagination-next"
href="/my/test/path/23?page=2&perPage=25"
navigate={[Function]}
>
<a
className="pagination-next"
href="/my/test/path/23?page=2&perPage=25"
onClick={[Function]}
>
Next page
</a>
</LinkAnchor>
</Link>
<ul
className="pagination-list"
>
<PageControl
current={true}
page={1}
url="/my/test/path/23?page=1&perPage=25"
>
<li>
<Link
aria-label="Goto page 1"
className="pagination-link is-current"
to="/my/test/path/23?page=1&perPage=25"
>
<LinkAnchor
aria-label="Goto page 1"
className="pagination-link is-current"
href="/my/test/path/23?page=1&perPage=25"
navigate={[Function]}
>
<a
aria-label="Goto page 1"
className="pagination-link is-current"
href="/my/test/path/23?page=1&perPage=25"
onClick={[Function]}
>
1
</a>
</LinkAnchor>
</Link>
</li>
</PageControl>
<PageControl
current={false}
key="page-2"
page={2}
url="/my/test/path/23?page=2&perPage=25"
>
<li>
<Link
aria-label="Goto page 2"
className="pagination-link"
to="/my/test/path/23?page=2&perPage=25"
>
<LinkAnchor
aria-label="Goto page 2"
className="pagination-link"
href="/my/test/path/23?page=2&perPage=25"
navigate={[Function]}
>
<a
aria-label="Goto page 2"
className="pagination-link"
href="/my/test/path/23?page=2&perPage=25"
onClick={[Function]}
>
2
</a>
</LinkAnchor>
</Link>
</li>
</PageControl>
<PageControl
current={false}
key="page-3"
page={3}
url="/my/test/path/23?page=3&perPage=25"
>
<li>
<Link
aria-label="Goto page 3"
className="pagination-link"
to="/my/test/path/23?page=3&perPage=25"
>
<LinkAnchor
aria-label="Goto page 3"
className="pagination-link"
href="/my/test/path/23?page=3&perPage=25"
navigate={[Function]}
>
<a
aria-label="Goto page 3"
className="pagination-link"
href="/my/test/path/23?page=3&perPage=25"
onClick={[Function]}
>
3
</a>
</LinkAnchor>
</Link>
</li>
</PageControl>
<PageControl
current={false}
key="page-4"
page={4}
url="/my/test/path/23?page=4&perPage=25"
>
<li>
<Link
aria-label="Goto page 4"
className="pagination-link"
to="/my/test/path/23?page=4&perPage=25"
>
<LinkAnchor
aria-label="Goto page 4"
className="pagination-link"
href="/my/test/path/23?page=4&perPage=25"
navigate={[Function]}
>
<a
aria-label="Goto page 4"
className="pagination-link"
href="/my/test/path/23?page=4&perPage=25"
onClick={[Function]}
>
4
</a>
</LinkAnchor>
</Link>
</li>
</PageControl>
<PageControl
current={false}
key="page-5"
page={5}
url="/my/test/path/23?page=5&perPage=25"
>
<li>
<Link
aria-label="Goto page 5"
className="pagination-link"
to="/my/test/path/23?page=5&perPage=25"
>
<LinkAnchor
aria-label="Goto page 5"
className="pagination-link"
href="/my/test/path/23?page=5&perPage=25"
navigate={[Function]}
>
<a
aria-label="Goto page 5"
className="pagination-link"
href="/my/test/path/23?page=5&perPage=25"
onClick={[Function]}
>
5
</a>
</LinkAnchor>
</Link>
</li>
</PageControl>
<PageControl
current={false}
key="page-6"
page={6}
url="/my/test/path/23?page=6&perPage=25"
>
<li>
<Link
aria-label="Goto page 6"
className="pagination-link"
to="/my/test/path/23?page=6&perPage=25"
>
<LinkAnchor
aria-label="Goto page 6"
className="pagination-link"
href="/my/test/path/23?page=6&perPage=25"
navigate={[Function]}
>
<a
aria-label="Goto page 6"
className="pagination-link"
href="/my/test/path/23?page=6&perPage=25"
onClick={[Function]}
>
6
</a>
</LinkAnchor>
</Link>
</li>
</PageControl>
<PageControl
current={false}
key="page-7"
page={7}
url="/my/test/path/23?page=7&perPage=25"
>
<li>
<Link
aria-label="Goto page 7"
className="pagination-link"
to="/my/test/path/23?page=7&perPage=25"
>
<LinkAnchor
aria-label="Goto page 7"
className="pagination-link"
href="/my/test/path/23?page=7&perPage=25"
navigate={[Function]}
>
<a
aria-label="Goto page 7"
className="pagination-link"
href="/my/test/path/23?page=7&perPage=25"
onClick={[Function]}
>
7
</a>
</LinkAnchor>
</Link>
</li>
</PageControl>
<li>
<span
className="pagination-ellipsis"
>
</span>
</li>
<PageControl
current={false}
page={11}
url="/my/test/path/23?page=11&perPage=25"
>
<li>
<Link
aria-label="Goto page 11"
className="pagination-link"
to="/my/test/path/23?page=11&perPage=25"
>
<LinkAnchor
aria-label="Goto page 11"
className="pagination-link"
href="/my/test/path/23?page=11&perPage=25"
navigate={[Function]}
>
<a
aria-label="Goto page 11"
className="pagination-link"
href="/my/test/path/23?page=11&perPage=25"
onClick={[Function]}
>
11
</a>
</LinkAnchor>
</Link>
</li>
</PageControl>
</ul>
</nav>
</Pagination>
`;

View file

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

View file

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

View file

@ -62,25 +62,25 @@ export const fetchTopicDetailsAction = createAsyncAction(
'GET_TOPIC_DETAILS__REQUEST',
'GET_TOPIC_DETAILS__SUCCESS',
'GET_TOPIC_DETAILS__FAILURE'
)<undefined, { topicName: TopicName; details: TopicDetails }, undefined>();
)<undefined, TopicsState, undefined>();
export const fetchTopicConfigAction = createAsyncAction(
'GET_TOPIC_CONFIG__REQUEST',
'GET_TOPIC_CONFIG__SUCCESS',
'GET_TOPIC_CONFIG__FAILURE'
)<undefined, { topicName: TopicName; config: TopicConfig[] }, undefined>();
)<undefined, TopicsState, undefined>();
export const createTopicAction = createAsyncAction(
'POST_TOPIC__REQUEST',
'POST_TOPIC__SUCCESS',
'POST_TOPIC__FAILURE'
)<undefined, Topic, undefined>();
)<undefined, TopicsState, undefined>();
export const updateTopicAction = createAsyncAction(
'PATCH_TOPIC__REQUEST',
'PATCH_TOPIC__SUCCESS',
'PATCH_TOPIC__FAILURE'
)<undefined, Topic, undefined>();
)<undefined, TopicsState, undefined>();
export const fetchConsumerGroupsAction = createAsyncAction(
'GET_CONSUMER_GROUPS__REQUEST',

View file

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

View file

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

View file

@ -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(