[CHORE] Pagination. Refactor topic reducer
This commit is contained in:
parent
bbdd60b7a5
commit
19cfdb07f2
21 changed files with 707 additions and 107 deletions
19
kafka-ui-react-app/package-lock.json
generated
19
kafka-ui-react-app/package-lock.json
generated
|
@ -19335,12 +19335,11 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ts-jest": {
|
"ts-jest": {
|
||||||
"version": "26.5.1",
|
"version": "26.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.3.tgz",
|
||||||
"integrity": "sha512-G7Rmo3OJMvlqE79amJX8VJKDiRcd7/r61wh9fnvvG8cAjhA9edklGw/dCxRSQmfZ/z8NDums5srSVgwZos1qfg==",
|
"integrity": "sha512-nBiiFGNvtujdLryU7MiMQh1iPmnZ/QvOskBbD2kURiI1MwqvxlxNnaAB/z9TbslMqCsSbu5BXvSSQPc5tvHGeA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/jest": "26.x",
|
|
||||||
"bs-logger": "0.x",
|
"bs-logger": "0.x",
|
||||||
"buffer-from": "1.x",
|
"buffer-from": "1.x",
|
||||||
"fast-json-stable-stringify": "2.x",
|
"fast-json-stable-stringify": "2.x",
|
||||||
|
@ -19494,9 +19493,9 @@
|
||||||
"integrity": "sha512-bna6Yi1pRznoo6Bz1cE6btB/Yy8Xywytyfrzu/wc+NFW3ZF0I+2iCGImhBsoYYCOWuICtRO4yHcnDlzgo1AdNg=="
|
"integrity": "sha512-bna6Yi1pRznoo6Bz1cE6btB/Yy8Xywytyfrzu/wc+NFW3ZF0I+2iCGImhBsoYYCOWuICtRO4yHcnDlzgo1AdNg=="
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "4.1.5",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
|
||||||
"integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==",
|
"integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"unicode-canonical-property-names-ecmascript": {
|
"unicode-canonical-property-names-ecmascript": {
|
||||||
|
@ -21622,9 +21621,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"yargs-parser": {
|
"yargs-parser": {
|
||||||
"version": "20.2.5",
|
"version": "20.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz",
|
||||||
"integrity": "sha512-jYRGS3zWy20NtDtK2kBgo/TlAoy5YUuhD9/LZ7z7W4j1Fdw2cqD0xEEclf8fxc8xjD6X5Qr+qQQwCEsP8iRiYg==",
|
"integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"yn": {
|
"yn": {
|
||||||
|
|
|
@ -112,9 +112,9 @@
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"react-scripts": "4.0.2",
|
"react-scripts": "4.0.2",
|
||||||
"redux-mock-store": "^1.5.4",
|
"redux-mock-store": "^1.5.4",
|
||||||
"ts-jest": "^26.5.1",
|
"ts-jest": "^26.5.3",
|
||||||
"ts-node": "^9.1.1",
|
"ts-node": "^9.1.1",
|
||||||
"typescript": "^4.1.5"
|
"typescript": "^4.2.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.15.4"
|
"node": ">=14.15.4"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { SchemaSubject } from 'generated-sources';
|
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 { clusterSchemaNewPath } from 'lib/paths';
|
||||||
import { ClusterName } from 'redux/interfaces';
|
import { ClusterName } from 'redux/interfaces';
|
||||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||||
|
@ -33,12 +33,12 @@ const List: React.FC<ListProps> = ({
|
||||||
<div className="level">
|
<div className="level">
|
||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
<div className="level-item level-right">
|
<div className="level-item level-right">
|
||||||
<NavLink
|
<Link
|
||||||
className="button is-primary"
|
className="button is-primary"
|
||||||
to={clusterSchemaNewPath(clusterName)}
|
to={clusterSchemaNewPath(clusterName)}
|
||||||
>
|
>
|
||||||
Create Schema
|
Create Schema
|
||||||
</NavLink>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -60,35 +60,21 @@ exports[`New View matches snapshot 1`] = `
|
||||||
<li
|
<li
|
||||||
key="/ui/clusters/undefined/schemas"
|
key="/ui/clusters/undefined/schemas"
|
||||||
>
|
>
|
||||||
<NavLink
|
<Link
|
||||||
to="/ui/clusters/undefined/schemas"
|
to="/ui/clusters/undefined/schemas"
|
||||||
>
|
>
|
||||||
<Link
|
<LinkAnchor
|
||||||
aria-current={null}
|
href="/ui/clusters/undefined/schemas"
|
||||||
to={
|
navigate={[Function]}
|
||||||
Object {
|
|
||||||
"hash": "",
|
|
||||||
"pathname": "/ui/clusters/undefined/schemas",
|
|
||||||
"search": "",
|
|
||||||
"state": null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<LinkAnchor
|
<a
|
||||||
aria-current={null}
|
|
||||||
href="/ui/clusters/undefined/schemas"
|
href="/ui/clusters/undefined/schemas"
|
||||||
navigate={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<a
|
Schema Registry
|
||||||
aria-current={null}
|
</a>
|
||||||
href="/ui/clusters/undefined/schemas"
|
</LinkAnchor>
|
||||||
onClick={[Function]}
|
</Link>
|
||||||
>
|
|
||||||
Schema Registry
|
|
||||||
</a>
|
|
||||||
</LinkAnchor>
|
|
||||||
</Link>
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="is-active"
|
className="is-active"
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TopicWithDetailedInfo, ClusterName } from 'redux/interfaces';
|
import { TopicWithDetailedInfo, ClusterName } from 'redux/interfaces';
|
||||||
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
|
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 { clusterTopicNewPath } from 'lib/paths';
|
||||||
import usePagination from 'lib/hooks/usePagination';
|
import usePagination from 'lib/hooks/usePagination';
|
||||||
import { FetchTopicsListParams } from 'redux/actions';
|
import { FetchTopicsListParams } from 'redux/actions';
|
||||||
import ClusterContext from 'components/contexts/ClusterContext';
|
import ClusterContext from 'components/contexts/ClusterContext';
|
||||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||||
|
import Pagination from 'components/common/Pagination/Pagination';
|
||||||
import ListItem from './ListItem';
|
import ListItem from './ListItem';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
areTopicsFetching: boolean;
|
areTopicsFetching: boolean;
|
||||||
topics: TopicWithDetailedInfo[];
|
topics: TopicWithDetailedInfo[];
|
||||||
externalTopics: TopicWithDetailedInfo[];
|
externalTopics: TopicWithDetailedInfo[];
|
||||||
|
totalPages: number;
|
||||||
fetchTopicsList(props: FetchTopicsListParams): void;
|
fetchTopicsList(props: FetchTopicsListParams): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +22,7 @@ const List: React.FC<Props> = ({
|
||||||
areTopicsFetching,
|
areTopicsFetching,
|
||||||
topics,
|
topics,
|
||||||
externalTopics,
|
externalTopics,
|
||||||
|
totalPages,
|
||||||
fetchTopicsList,
|
fetchTopicsList,
|
||||||
}) => {
|
}) => {
|
||||||
const { isReadOnly } = React.useContext(ClusterContext);
|
const { isReadOnly } = React.useContext(ClusterContext);
|
||||||
|
@ -58,12 +61,12 @@ const List: React.FC<Props> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="level-item level-right">
|
<div className="level-item level-right">
|
||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
<NavLink
|
<Link
|
||||||
className="button is-primary"
|
className="button is-primary"
|
||||||
to={clusterTopicNewPath(clusterName)}
|
to={clusterTopicNewPath(clusterName)}
|
||||||
>
|
>
|
||||||
Add a Topic
|
Add a Topic
|
||||||
</NavLink>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -93,6 +96,7 @@ const List: React.FC<Props> = ({
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<Pagination totalPages={totalPages} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
getTopicList,
|
getTopicList,
|
||||||
getExternalTopicList,
|
getExternalTopicList,
|
||||||
getAreTopicsFetching,
|
getAreTopicsFetching,
|
||||||
|
getTopicListTotalPages,
|
||||||
} from 'redux/reducers/topics/selectors';
|
} from 'redux/reducers/topics/selectors';
|
||||||
import List from './List';
|
import List from './List';
|
||||||
|
|
||||||
|
@ -12,6 +13,7 @@ const mapStateToProps = (state: RootState) => ({
|
||||||
areTopicsFetching: getAreTopicsFetching(state),
|
areTopicsFetching: getAreTopicsFetching(state),
|
||||||
topics: getTopicList(state),
|
topics: getTopicList(state),
|
||||||
externalTopics: getExternalTopicList(state),
|
externalTopics: getExternalTopicList(state),
|
||||||
|
totalPages: getTopicListTotalPages(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { mount } from 'enzyme';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
import { StaticRouter } from 'react-router-dom';
|
import { StaticRouter } from 'react-router-dom';
|
||||||
import ClusterContext from 'components/contexts/ClusterContext';
|
import ClusterContext from 'components/contexts/ClusterContext';
|
||||||
import List from '../List';
|
import List from '../List';
|
||||||
|
@ -14,12 +14,13 @@ describe('List', () => {
|
||||||
areTopicsFetching={false}
|
areTopicsFetching={false}
|
||||||
topics={[]}
|
topics={[]}
|
||||||
externalTopics={[]}
|
externalTopics={[]}
|
||||||
|
totalPages={1}
|
||||||
fetchTopicsList={jest.fn()}
|
fetchTopicsList={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</ClusterContext.Provider>
|
</ClusterContext.Provider>
|
||||||
</StaticRouter>
|
</StaticRouter>
|
||||||
);
|
);
|
||||||
expect(component.exists('NavLink')).toBeFalsy();
|
expect(component.exists('Link')).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -32,12 +33,13 @@ describe('List', () => {
|
||||||
areTopicsFetching={false}
|
areTopicsFetching={false}
|
||||||
topics={[]}
|
topics={[]}
|
||||||
externalTopics={[]}
|
externalTopics={[]}
|
||||||
|
totalPages={1}
|
||||||
fetchTopicsList={jest.fn()}
|
fetchTopicsList={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</ClusterContext.Provider>
|
</ClusterContext.Provider>
|
||||||
</StaticRouter>
|
</StaticRouter>
|
||||||
);
|
);
|
||||||
expect(component.exists('NavLink')).toBeTruthy();
|
expect(component.exists('Link')).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
export interface Link {
|
export interface BreadcrumbItem {
|
||||||
label: string;
|
label: string;
|
||||||
href: string;
|
href: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
links?: Link[];
|
links?: BreadcrumbItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Breadcrumb: React.FC<Props> = ({ links, children }) => {
|
const Breadcrumb: React.FC<Props> = ({ links, children }) => {
|
||||||
|
@ -17,7 +17,7 @@ const Breadcrumb: React.FC<Props> = ({ links, children }) => {
|
||||||
{links &&
|
{links &&
|
||||||
links.map(({ label, href }) => (
|
links.map(({ label, href }) => (
|
||||||
<li key={href}>
|
<li key={href}>
|
||||||
<NavLink to={href}>{label}</NavLink>
|
<Link to={href}>{label}</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { mount, shallow } from 'enzyme';
|
import { mount, shallow } from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { StaticRouter } from 'react-router-dom';
|
import { StaticRouter } from 'react-router-dom';
|
||||||
import Breadcrumb, { Link } from '../Breadcrumb';
|
import Breadcrumb, { BreadcrumbItem } from '../Breadcrumb';
|
||||||
|
|
||||||
describe('Breadcrumb component', () => {
|
describe('Breadcrumb component', () => {
|
||||||
const links: Link[] = [
|
const links: BreadcrumbItem[] = [
|
||||||
{
|
{
|
||||||
label: 'link1',
|
label: 'link1',
|
||||||
href: 'link1href',
|
href: 'link1href',
|
||||||
|
@ -28,7 +28,7 @@ describe('Breadcrumb component', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
it('renders the list of links', () => {
|
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.prop('to')).toEqual(links[idx].href);
|
||||||
expect(link.contains(links[idx].label)).toBeTruthy();
|
expect(link.contains(links[idx].label)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
|
@ -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">…</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">…</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{!pages.includes(totalPages) && (
|
||||||
|
<PageControl
|
||||||
|
page={totalPages}
|
||||||
|
current={currentPage === totalPages}
|
||||||
|
url={getPath(totalPages)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pagination;
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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
|
|
@ -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>
|
||||||
|
`;
|
|
@ -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>
|
||||||
|
`;
|
|
@ -16,3 +16,5 @@ export const MILLISECONDS_IN_DAY = 86_400_000;
|
||||||
export const MILLISECONDS_IN_SECOND = 1_000;
|
export const MILLISECONDS_IN_SECOND = 1_000;
|
||||||
|
|
||||||
export const BYTES_IN_GB = 1_073_741_824;
|
export const BYTES_IN_GB = 1_073_741_824;
|
||||||
|
|
||||||
|
export const PER_PAGE = 25;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
|
|
||||||
const usePagination = () => {
|
const usePagination = () => {
|
||||||
const params = new URLSearchParams(useLocation().search);
|
const { search, pathname } = useLocation();
|
||||||
|
const params = new URLSearchParams(search);
|
||||||
|
|
||||||
const page = params.get('page');
|
const page = params.get('page');
|
||||||
const perPage = params.get('perPage');
|
const perPage = params.get('perPage');
|
||||||
|
@ -9,6 +10,7 @@ const usePagination = () => {
|
||||||
return {
|
return {
|
||||||
page: page ? Number(page) : undefined,
|
page: page ? Number(page) : undefined,
|
||||||
perPage: perPage ? Number(perPage) : undefined,
|
perPage: perPage ? Number(perPage) : undefined,
|
||||||
|
pathname,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -62,25 +62,25 @@ export const fetchTopicDetailsAction = createAsyncAction(
|
||||||
'GET_TOPIC_DETAILS__REQUEST',
|
'GET_TOPIC_DETAILS__REQUEST',
|
||||||
'GET_TOPIC_DETAILS__SUCCESS',
|
'GET_TOPIC_DETAILS__SUCCESS',
|
||||||
'GET_TOPIC_DETAILS__FAILURE'
|
'GET_TOPIC_DETAILS__FAILURE'
|
||||||
)<undefined, { topicName: TopicName; details: TopicDetails }, undefined>();
|
)<undefined, TopicsState, undefined>();
|
||||||
|
|
||||||
export const fetchTopicConfigAction = createAsyncAction(
|
export const fetchTopicConfigAction = createAsyncAction(
|
||||||
'GET_TOPIC_CONFIG__REQUEST',
|
'GET_TOPIC_CONFIG__REQUEST',
|
||||||
'GET_TOPIC_CONFIG__SUCCESS',
|
'GET_TOPIC_CONFIG__SUCCESS',
|
||||||
'GET_TOPIC_CONFIG__FAILURE'
|
'GET_TOPIC_CONFIG__FAILURE'
|
||||||
)<undefined, { topicName: TopicName; config: TopicConfig[] }, undefined>();
|
)<undefined, TopicsState, undefined>();
|
||||||
|
|
||||||
export const createTopicAction = createAsyncAction(
|
export const createTopicAction = createAsyncAction(
|
||||||
'POST_TOPIC__REQUEST',
|
'POST_TOPIC__REQUEST',
|
||||||
'POST_TOPIC__SUCCESS',
|
'POST_TOPIC__SUCCESS',
|
||||||
'POST_TOPIC__FAILURE'
|
'POST_TOPIC__FAILURE'
|
||||||
)<undefined, Topic, undefined>();
|
)<undefined, TopicsState, undefined>();
|
||||||
|
|
||||||
export const updateTopicAction = createAsyncAction(
|
export const updateTopicAction = createAsyncAction(
|
||||||
'PATCH_TOPIC__REQUEST',
|
'PATCH_TOPIC__REQUEST',
|
||||||
'PATCH_TOPIC__SUCCESS',
|
'PATCH_TOPIC__SUCCESS',
|
||||||
'PATCH_TOPIC__FAILURE'
|
'PATCH_TOPIC__FAILURE'
|
||||||
)<undefined, Topic, undefined>();
|
)<undefined, TopicsState, undefined>();
|
||||||
|
|
||||||
export const fetchConsumerGroupsAction = createAsyncAction(
|
export const fetchConsumerGroupsAction = createAsyncAction(
|
||||||
'GET_CONSUMER_GROUPS__REQUEST',
|
'GET_CONSUMER_GROUPS__REQUEST',
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
TopicFormDataRaw,
|
TopicFormDataRaw,
|
||||||
TopicsState,
|
TopicsState,
|
||||||
} from 'redux/interfaces';
|
} from 'redux/interfaces';
|
||||||
|
|
||||||
import { BASE_PARAMS } from 'lib/constants';
|
import { BASE_PARAMS } from 'lib/constants';
|
||||||
import * as actions from '../actions';
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
@ -82,19 +81,25 @@ export const fetchTopicMessages = (
|
||||||
export const fetchTopicDetails = (
|
export const fetchTopicDetails = (
|
||||||
clusterName: ClusterName,
|
clusterName: ClusterName,
|
||||||
topicName: TopicName
|
topicName: TopicName
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
): PromiseThunkResult => async (dispatch, getState) => {
|
||||||
dispatch(actions.fetchTopicDetailsAction.request());
|
dispatch(actions.fetchTopicDetailsAction.request());
|
||||||
try {
|
try {
|
||||||
const topicDetails = await topicsApiClient.getTopicDetails({
|
const topicDetails = await topicsApiClient.getTopicDetails({
|
||||||
clusterName,
|
clusterName,
|
||||||
topicName,
|
topicName,
|
||||||
});
|
});
|
||||||
dispatch(
|
const state = getState().topics;
|
||||||
actions.fetchTopicDetailsAction.success({
|
const newState = {
|
||||||
topicName,
|
...state,
|
||||||
details: topicDetails,
|
byName: {
|
||||||
})
|
...state.byName,
|
||||||
);
|
[topicName]: {
|
||||||
|
...state.byName[topicName],
|
||||||
|
...topicDetails,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
dispatch(actions.fetchTopicDetailsAction.success(newState));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch(actions.fetchTopicDetailsAction.failure());
|
dispatch(actions.fetchTopicDetailsAction.failure());
|
||||||
}
|
}
|
||||||
|
@ -103,14 +108,29 @@ export const fetchTopicDetails = (
|
||||||
export const fetchTopicConfig = (
|
export const fetchTopicConfig = (
|
||||||
clusterName: ClusterName,
|
clusterName: ClusterName,
|
||||||
topicName: TopicName
|
topicName: TopicName
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
): PromiseThunkResult => async (dispatch, getState) => {
|
||||||
dispatch(actions.fetchTopicConfigAction.request());
|
dispatch(actions.fetchTopicConfigAction.request());
|
||||||
try {
|
try {
|
||||||
const config = await topicsApiClient.getTopicConfigs({
|
const config = await topicsApiClient.getTopicConfigs({
|
||||||
clusterName,
|
clusterName,
|
||||||
topicName,
|
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) {
|
} catch (e) {
|
||||||
dispatch(actions.fetchTopicConfigAction.failure());
|
dispatch(actions.fetchTopicConfigAction.failure());
|
||||||
}
|
}
|
||||||
|
@ -155,14 +175,27 @@ const formatTopicFormData = (form: TopicFormDataRaw): TopicFormData => {
|
||||||
export const createTopic = (
|
export const createTopic = (
|
||||||
clusterName: ClusterName,
|
clusterName: ClusterName,
|
||||||
form: TopicFormDataRaw
|
form: TopicFormDataRaw
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
): PromiseThunkResult => async (dispatch, getState) => {
|
||||||
dispatch(actions.createTopicAction.request());
|
dispatch(actions.createTopicAction.request());
|
||||||
try {
|
try {
|
||||||
const topic: Topic = await topicsApiClient.createTopic({
|
const topic: Topic = await topicsApiClient.createTopic({
|
||||||
clusterName,
|
clusterName,
|
||||||
topicFormData: formatTopicFormData(form),
|
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) {
|
} catch (e) {
|
||||||
dispatch(actions.createTopicAction.failure());
|
dispatch(actions.createTopicAction.failure());
|
||||||
}
|
}
|
||||||
|
@ -171,7 +204,7 @@ export const createTopic = (
|
||||||
export const updateTopic = (
|
export const updateTopic = (
|
||||||
clusterName: ClusterName,
|
clusterName: ClusterName,
|
||||||
form: TopicFormDataRaw
|
form: TopicFormDataRaw
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
): PromiseThunkResult => async (dispatch, getState) => {
|
||||||
dispatch(actions.updateTopicAction.request());
|
dispatch(actions.updateTopicAction.request());
|
||||||
try {
|
try {
|
||||||
const topic: Topic = await topicsApiClient.updateTopic({
|
const topic: Topic = await topicsApiClient.updateTopic({
|
||||||
|
@ -179,7 +212,20 @@ export const updateTopic = (
|
||||||
topicName: form.name,
|
topicName: form.name,
|
||||||
topicFormData: formatTopicFormData(form),
|
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) {
|
} catch (e) {
|
||||||
dispatch(actions.updateTopicAction.failure());
|
dispatch(actions.updateTopicAction.failure());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Topic, TopicMessage } from 'generated-sources';
|
import { TopicMessage } from 'generated-sources';
|
||||||
import { Action, TopicsState } from 'redux/interfaces';
|
import { Action, TopicsState } from 'redux/interfaces';
|
||||||
import { getType } from 'typesafe-actions';
|
import { getType } from 'typesafe-actions';
|
||||||
import * as actions from 'redux/actions';
|
import * as actions from 'redux/actions';
|
||||||
|
@ -10,15 +10,6 @@ export const initialState: TopicsState = {
|
||||||
messages: [],
|
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 = (
|
const transformTopicMessages = (
|
||||||
state: TopicsState,
|
state: TopicsState,
|
||||||
messages: TopicMessage[]
|
messages: TopicMessage[]
|
||||||
|
@ -47,35 +38,13 @@ const transformTopicMessages = (
|
||||||
const reducer = (state = initialState, action: Action): TopicsState => {
|
const reducer = (state = initialState, action: Action): TopicsState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case getType(actions.fetchTopicsListAction.success):
|
case getType(actions.fetchTopicsListAction.success):
|
||||||
return action.payload;
|
|
||||||
case getType(actions.fetchTopicDetailsAction.success):
|
case getType(actions.fetchTopicDetailsAction.success):
|
||||||
return {
|
case getType(actions.fetchTopicConfigAction.success):
|
||||||
...state,
|
case getType(actions.createTopicAction.success):
|
||||||
byName: {
|
case getType(actions.updateTopicAction.success):
|
||||||
...state.byName,
|
return action.payload;
|
||||||
[action.payload.topicName]: {
|
|
||||||
...state.byName[action.payload.topicName],
|
|
||||||
...action.payload.details,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
case getType(actions.fetchTopicMessagesAction.success):
|
case getType(actions.fetchTopicMessagesAction.success):
|
||||||
return transformTopicMessages(state, action.payload);
|
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:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@ const getAllNames = (state: RootState) => topicsState(state).allNames;
|
||||||
const getTopicMap = (state: RootState) => topicsState(state).byName;
|
const getTopicMap = (state: RootState) => topicsState(state).byName;
|
||||||
export const getTopicMessages = (state: RootState) =>
|
export const getTopicMessages = (state: RootState) =>
|
||||||
topicsState(state).messages;
|
topicsState(state).messages;
|
||||||
|
export const getTopicListTotalPages = (state: RootState) =>
|
||||||
|
topicsState(state).totalPages;
|
||||||
|
|
||||||
const getTopicListFetchingStatus = createFetchingSelector('GET_TOPICS');
|
const getTopicListFetchingStatus = createFetchingSelector('GET_TOPICS');
|
||||||
const getTopicDetailsFetchingStatus = createFetchingSelector(
|
const getTopicDetailsFetchingStatus = createFetchingSelector(
|
||||||
|
|
Loading…
Add table
Reference in a new issue