parent
bbdd60b7a5
commit
49a7564366
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
|
||||
},
|
||||
"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": {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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 BYTES_IN_GB = 1_073_741_824;
|
||||
|
||||
export const PER_PAGE = 25;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Reference in a new issue