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:
commit
ec70e28bff
8 changed files with 189 additions and 110 deletions
44
kafka-ui-react-app/package-lock.json
generated
44
kafka-ui-react-app/package-lock.json
generated
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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=""
|
||||
>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Reference in a new issue