Merge pull request #746 from provectus/#710/internal-topics-repagination

[#710] feat(react-app): call fetchTopics on toggle show internal topics
This commit is contained in:
Ilnur Farukhshin 2021-07-29 21:45:06 +03:00 committed by GitHub
commit ec70e28bff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 189 additions and 110 deletions

View file

@ -9710,16 +9710,12 @@
"dev": true
},
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.0.0.tgz",
"integrity": "sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg==",
"dev": true,
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
"@babel/runtime": "^7.7.6"
}
},
"hmac-drbg": {
@ -16643,6 +16639,21 @@
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"dependencies": {
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
}
}
},
"react-router-dom": {
@ -16657,6 +16668,21 @@
"react-router": "5.2.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"dependencies": {
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
}
}
},
"react-scripts": {

View file

@ -109,6 +109,7 @@
"eslint-plugin-react-hooks": "^4.2.0",
"esprint": "^3.1.0",
"fetch-mock-jest": "^1.5.1",
"history": "^5.0.0",
"husky": "^7.0.0",
"jest-sonar-reporter": "^2.0.0",
"lint-staged": "^11.0.0",

View file

@ -1,4 +1,5 @@
import React from 'react';
import { useHistory } from 'react-router';
import {
TopicWithDetailedInfo,
ClusterName,
@ -8,22 +9,21 @@ import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
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 { TopicColumnsToSort } from 'generated-sources';
import { GetTopicsRequest, TopicColumnsToSort } from 'generated-sources';
import SortableColumnHeader from 'components/common/table/SortableCulumnHeader/SortableColumnHeader';
import Search from 'components/common/Search/Search';
import { PER_PAGE } from 'lib/constants';
import ListItem from './ListItem';
interface Props {
export interface TopicsListProps {
areTopicsFetching: boolean;
topics: TopicWithDetailedInfo[];
externalTopics: TopicWithDetailedInfo[];
totalPages: number;
fetchTopicsList(props: FetchTopicsListParams): void;
fetchTopicsList(props: GetTopicsRequest): void;
deleteTopic(topicName: TopicName, clusterName: ClusterName): void;
clearTopicMessages(
topicName: TopicName,
@ -36,10 +36,9 @@ interface Props {
setTopicsOrderBy(orderBy: TopicColumnsToSort | null): void;
}
const List: React.FC<Props> = ({
const List: React.FC<TopicsListProps> = ({
areTopicsFetching,
topics,
externalTopics,
totalPages,
fetchTopicsList,
deleteTopic,
@ -51,7 +50,9 @@ const List: React.FC<Props> = ({
}) => {
const { isReadOnly } = React.useContext(ClusterContext);
const { clusterName } = useParams<{ clusterName: ClusterName }>();
const { page, perPage } = usePagination();
const { page, perPage, pathname } = usePagination();
const [showInternal, setShowInternal] = React.useState<boolean>(true);
const history = useHistory();
React.useEffect(() => {
fetchTopicsList({
@ -60,19 +61,23 @@ const List: React.FC<Props> = ({
perPage,
orderBy: orderBy || undefined,
search,
showInternal,
});
}, [fetchTopicsList, clusterName, page, perPage, orderBy, search]);
const [showInternal, setShowInternal] = React.useState<boolean>(true);
}, [
fetchTopicsList,
clusterName,
page,
perPage,
orderBy,
search,
showInternal,
]);
const handleSwitch = React.useCallback(() => {
setShowInternal(!showInternal);
history.push(`${pathname}?page=1&perPage=${perPage || PER_PAGE}`);
}, [showInternal]);
const handleSearch = (value: string) => setTopicsSearch(value);
const items = showInternal ? topics : externalTopics;
return (
<div className="section">
<Breadcrumb>{showInternal ? `All Topics` : `External Topics`}</Breadcrumb>
@ -93,7 +98,7 @@ const List: React.FC<Props> = ({
</div>
<div className="column">
<Search
handleSearch={handleSearch}
handleSearch={setTopicsSearch}
placeholder="Search by Topic Name"
value={search}
/>
@ -144,7 +149,7 @@ const List: React.FC<Props> = ({
</tr>
</thead>
<tbody>
{items.map((topic) => (
{topics.map((topic) => (
<ListItem
clusterName={clusterName}
key={topic.name}
@ -153,7 +158,7 @@ const List: React.FC<Props> = ({
clearTopicMessages={clearTopicMessages}
/>
))}
{items.length === 0 && (
{topics.length === 0 && (
<tr>
<td colSpan={10}>No topics found</td>
</tr>

View file

@ -9,7 +9,6 @@ import {
} from 'redux/actions';
import {
getTopicList,
getExternalTopicList,
getAreTopicsFetching,
getTopicListTotalPages,
getTopicsSearch,
@ -21,7 +20,6 @@ import List from './List';
const mapStateToProps = (state: RootState) => ({
areTopicsFetching: getAreTopicsFetching(state),
topics: getTopicList(state),
externalTopics: getExternalTopicList(state),
totalPages: getTopicListTotalPages(state),
search: getTopicsSearch(state),
orderBy: getTopicsOrderBy(state),

View file

@ -1,84 +1,126 @@
import React from 'react';
import { mount } from 'enzyme';
import { StaticRouter } from 'react-router-dom';
import ClusterContext from 'components/contexts/ClusterContext';
import List from 'components/Topics/List/List';
import { mount, ReactWrapper } from 'enzyme';
import { Router } from 'react-router-dom';
import ClusterContext, {
ContextProps,
} from 'components/contexts/ClusterContext';
import List, { TopicsListProps } from 'components/Topics/List/List';
import { createMemoryHistory } from 'history';
import { StaticRouter } from 'react-router';
import Search from 'components/common/Search/Search';
describe('List', () => {
const setupComponent = (props: Partial<TopicsListProps> = {}) => (
<List
areTopicsFetching={false}
topics={[]}
totalPages={1}
fetchTopicsList={jest.fn()}
deleteTopic={jest.fn()}
clearTopicMessages={jest.fn()}
search=""
orderBy={null}
setTopicsSearch={jest.fn()}
setTopicsOrderBy={jest.fn()}
{...props}
/>
);
const historyMock = createMemoryHistory();
const mountComponentWithProviders = (
contextProps: Partial<ContextProps> = {},
props: Partial<TopicsListProps> = {},
history = historyMock
) =>
mount(
<Router history={history}>
<ClusterContext.Provider
value={{
isReadOnly: true,
hasKafkaConnectConfigured: true,
hasSchemaRegistryConfigured: true,
...contextProps,
}}
>
{setupComponent(props)}
</ClusterContext.Provider>
</Router>
);
describe('when it has readonly flag', () => {
it('does not render the Add a Topic button', () => {
const component = mount(
<StaticRouter>
<ClusterContext.Provider
value={{
isReadOnly: true,
hasKafkaConnectConfigured: true,
hasSchemaRegistryConfigured: true,
}}
>
<List
areTopicsFetching={false}
topics={[]}
externalTopics={[]}
totalPages={1}
fetchTopicsList={jest.fn()}
deleteTopic={jest.fn()}
clearTopicMessages={jest.fn()}
search=""
orderBy={null}
setTopicsSearch={jest.fn()}
setTopicsOrderBy={jest.fn()}
/>
</ClusterContext.Provider>
</StaticRouter>
);
const component = mountComponentWithProviders();
expect(component.exists('Link')).toBeFalsy();
});
});
describe('when it does not have readonly flag', () => {
const mockFetch = jest.fn();
let fetchTopicsList = jest.fn();
let component: ReactWrapper;
jest.useFakeTimers();
const component = mount(
<StaticRouter>
<ClusterContext.Provider
value={{
isReadOnly: false,
hasKafkaConnectConfigured: true,
hasSchemaRegistryConfigured: true,
}}
>
<List
areTopicsFetching={false}
topics={[]}
externalTopics={[]}
totalPages={1}
fetchTopicsList={mockFetch}
deleteTopic={jest.fn()}
clearTopicMessages={jest.fn()}
search=""
orderBy={null}
setTopicsSearch={jest.fn()}
setTopicsOrderBy={jest.fn()}
/>
</ClusterContext.Provider>
</StaticRouter>
);
beforeEach(() => {
fetchTopicsList = jest.fn();
component = mountComponentWithProviders(
{ isReadOnly: false },
{ fetchTopicsList }
);
});
it('renders the Add a Topic button', () => {
expect(component.exists('Link')).toBeTruthy();
});
it('matches the snapshot', () => {
component = mount(
<StaticRouter>
<ClusterContext.Provider
value={{
isReadOnly: false,
hasKafkaConnectConfigured: true,
hasSchemaRegistryConfigured: true,
}}
>
{setupComponent()}
</ClusterContext.Provider>
</StaticRouter>
);
expect(component).toMatchSnapshot();
});
it('calls fetchTopicsList on input', () => {
const input = component.find('input').at(1);
input.simulate('change', { target: { value: 't' } });
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 300);
setTimeout(() => {
expect(mockFetch).toHaveBeenCalledTimes(1);
}, 301);
it('calls setTopicsSearch on input', () => {
const setTopicsSearch = jest.fn();
component = mountComponentWithProviders({}, { setTopicsSearch });
const query = 'topic';
const input = component.find(Search);
input.props().handleSearch(query);
expect(setTopicsSearch).toHaveBeenCalledWith(query);
});
it('should refetch topics on show internal toggle change', () => {
jest.clearAllMocks();
const toggle = component.find('input#switchRoundedDefault');
toggle.simulate('change');
expect(fetchTopicsList).toHaveBeenLastCalledWith({
search: '',
showInternal: false,
});
});
it('should reset page query param on show internal toggle change', () => {
const mockedHistory = createMemoryHistory();
jest.spyOn(mockedHistory, 'push');
component = mountComponentWithProviders(
{ isReadOnly: false },
{ fetchTopicsList },
mockedHistory
);
const toggle = component.find('input#switchRoundedDefault');
toggle.simulate('change');
expect(mockedHistory.push).toHaveBeenCalledWith('/?page=1&perPage=25');
});
});
});

View file

@ -28,8 +28,28 @@ exports[`List when it does not have readonly flag matches the snapshot 1`] = `
areTopicsFetching={false}
clearTopicMessages={[MockFunction]}
deleteTopic={[MockFunction]}
externalTopics={Array []}
fetchTopicsList={[MockFunction]}
fetchTopicsList={
[MockFunction] {
"calls": Array [
Array [
Object {
"clusterName": undefined,
"orderBy": undefined,
"page": undefined,
"perPage": undefined,
"search": "",
"showInternal": true,
},
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
}
orderBy={null}
search=""
setTopicsOrderBy={[MockFunction]}
@ -89,7 +109,7 @@ exports[`List when it does not have readonly flag matches the snapshot 1`] = `
className="column"
>
<Search
handleSearch={[Function]}
handleSearch={[MockFunction]}
placeholder="Search by Topic Name"
value=""
>

View file

@ -7,9 +7,9 @@ import {
TopicCreation,
TopicUpdate,
TopicConfig,
TopicColumnsToSort,
ConsumerGroupsApi,
CreateTopicMessage,
GetTopicsRequest,
} from 'generated-sources';
import {
PromiseThunkResult,
@ -32,17 +32,8 @@ export const topicConsumerGroupsApiClient = new ConsumerGroupsApi(
apiClientConf
);
export interface FetchTopicsListParams {
clusterName: ClusterName;
page?: number;
perPage?: number;
showInternal?: boolean;
search?: string;
orderBy?: TopicColumnsToSort;
}
export const fetchTopicsList =
(params: FetchTopicsListParams): PromiseThunkResult =>
(params: GetTopicsRequest): PromiseThunkResult =>
async (dispatch, getState) => {
dispatch(actions.fetchTopicsListAction.request());
try {

View file

@ -112,10 +112,6 @@ export const getTopicList = createSelector(
}
);
export const getExternalTopicList = createSelector(getTopicList, (topics) =>
topics.filter(({ internal }) => !internal)
);
const getTopicName = (_: RootState, topicName: TopicName) => topicName;
export const getTopicByName = createSelector(