From bbdd60b7a590e1843db9868833d6a47b68d3819f Mon Sep 17 00:00:00 2001 From: Oleg Shur Date: Tue, 16 Mar 2021 15:13:21 +0300 Subject: [PATCH] Topics page refactoring (#251) * Split thunks on files. Refactoring * [CHORE] Refactor Topics section --- .../src/components/Cluster/Cluster.tsx | 7 +- .../Topics/Details/Overview/Overview.tsx | 91 ----- .../Details/Settings/SettingsEditButton.tsx | 16 - .../components/Topics/Edit/EditContainer.tsx | 64 ---- .../src/components/Topics/List/List.tsx | 76 +++-- .../components/Topics/List/ListContainer.ts | 28 +- .../Topics/List/__tests__/List.spec.tsx | 37 +- .../Topics/{ => Topic}/Details/Details.tsx | 79 ++--- .../{ => Topic}/Details/DetailsContainer.ts | 0 .../Details/Messages/MessageItem.tsx | 0 .../{ => Topic}/Details/Messages/Messages.tsx | 0 .../Details/Messages/MessagesContainer.ts | 0 .../Details/Messages/MessagesTable.tsx | 0 .../Messages/__test__/MessageItem.spec.tsx | 2 +- .../Messages/__test__/Messages.spec.tsx | 6 +- .../Messages/__test__/MessagesTable.spec.tsx | 2 +- .../__snapshots__/MessageItem.spec.tsx.snap | 0 .../__snapshots__/MessagesTable.spec.tsx.snap | 0 .../Details/Messages/__test__/fixtures.ts | 0 .../Topic/Details/Overview/Overview.tsx | 70 ++++ .../Details/Overview/OverviewContainer.ts | 25 ++ .../{ => Topic}/Details/Settings/Settings.tsx | 5 +- .../Details/Settings/SettingsContainer.ts | 0 .../Topics/{ => Topic}/Edit/Edit.tsx | 51 +-- .../Edit/EditContainer.tsx} | 29 +- .../src/components/Topics/Topic/Topic.tsx | 80 +++++ .../Topics/Topic/TopicContainer.tsx | 15 + .../src/components/Topics/Topics.tsx | 66 ++-- .../src/components/Topics/TopicsContainer.ts | 30 -- .../shared/Form/CustomParams/CustomParams.tsx | 2 +- .../src/lib/hooks/usePagination.ts | 15 + .../src/redux/actions/actions.ts | 4 +- .../src/redux/actions/thunks.ts | 318 ------------------ .../src/redux/actions/thunks/brokers.ts | 36 ++ .../src/redux/actions/thunks/clusters.ts | 42 +++ .../redux/actions/thunks/consumerGroups.ts | 49 +++ .../src/redux/actions/thunks/index.ts | 5 + .../src/redux/actions/thunks/schemas.ts | 59 ++++ .../src/redux/actions/thunks/topics.ts | 186 ++++++++++ .../src/redux/interfaces/topic.ts | 8 +- .../src/redux/reducers/topics/reducer.ts | 30 +- .../src/redux/reducers/topics/selectors.ts | 14 +- 42 files changed, 790 insertions(+), 757 deletions(-) delete mode 100644 kafka-ui-react-app/src/components/Topics/Details/Overview/Overview.tsx delete mode 100644 kafka-ui-react-app/src/components/Topics/Details/Settings/SettingsEditButton.tsx delete mode 100644 kafka-ui-react-app/src/components/Topics/Edit/EditContainer.tsx rename kafka-ui-react-app/src/components/Topics/{ => Topic}/Details/Details.tsx (54%) rename kafka-ui-react-app/src/components/Topics/{ => Topic}/Details/DetailsContainer.ts (100%) rename kafka-ui-react-app/src/components/Topics/{ => Topic}/Details/Messages/MessageItem.tsx (100%) rename kafka-ui-react-app/src/components/Topics/{ => Topic}/Details/Messages/Messages.tsx (100%) rename kafka-ui-react-app/src/components/Topics/{ => Topic}/Details/Messages/MessagesContainer.ts (100%) rename kafka-ui-react-app/src/components/Topics/{ => Topic}/Details/Messages/MessagesTable.tsx (100%) rename kafka-ui-react-app/src/components/Topics/{ => Topic}/Details/Messages/__test__/MessageItem.spec.tsx (93%) rename kafka-ui-react-app/src/components/Topics/{ => Topic}/Details/Messages/__test__/Messages.spec.tsx (96%) rename kafka-ui-react-app/src/components/Topics/{ => Topic}/Details/Messages/__test__/MessagesTable.spec.tsx (95%) rename kafka-ui-react-app/src/components/Topics/{ => Topic}/Details/Messages/__test__/__snapshots__/MessageItem.spec.tsx.snap (100%) rename kafka-ui-react-app/src/components/Topics/{ => Topic}/Details/Messages/__test__/__snapshots__/MessagesTable.spec.tsx.snap (100%) rename kafka-ui-react-app/src/components/Topics/{ => Topic}/Details/Messages/__test__/fixtures.ts (100%) create mode 100644 kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx create mode 100644 kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/OverviewContainer.ts rename kafka-ui-react-app/src/components/Topics/{ => Topic}/Details/Settings/Settings.tsx (89%) rename kafka-ui-react-app/src/components/Topics/{ => Topic}/Details/Settings/SettingsContainer.ts (100%) rename kafka-ui-react-app/src/components/Topics/{ => Topic}/Edit/Edit.tsx (65%) rename kafka-ui-react-app/src/components/Topics/{Details/Overview/OverviewContainer.ts => Topic/Edit/EditContainer.tsx} (50%) create mode 100644 kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx create mode 100644 kafka-ui-react-app/src/components/Topics/Topic/TopicContainer.tsx delete mode 100644 kafka-ui-react-app/src/components/Topics/TopicsContainer.ts create mode 100644 kafka-ui-react-app/src/lib/hooks/usePagination.ts delete mode 100644 kafka-ui-react-app/src/redux/actions/thunks.ts create mode 100644 kafka-ui-react-app/src/redux/actions/thunks/brokers.ts create mode 100644 kafka-ui-react-app/src/redux/actions/thunks/clusters.ts create mode 100644 kafka-ui-react-app/src/redux/actions/thunks/consumerGroups.ts create mode 100644 kafka-ui-react-app/src/redux/actions/thunks/index.ts create mode 100644 kafka-ui-react-app/src/redux/actions/thunks/schemas.ts create mode 100644 kafka-ui-react-app/src/redux/actions/thunks/topics.ts diff --git a/kafka-ui-react-app/src/components/Cluster/Cluster.tsx b/kafka-ui-react-app/src/components/Cluster/Cluster.tsx index fc5c157937..3461410e4d 100644 --- a/kafka-ui-react-app/src/components/Cluster/Cluster.tsx +++ b/kafka-ui-react-app/src/components/Cluster/Cluster.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { Switch, Route, Redirect, useParams } from 'react-router-dom'; import BrokersContainer from 'components/Brokers/BrokersContainer'; -import TopicsContainer from 'components/Topics/TopicsContainer'; +import Topics from 'components/Topics/Topics'; import ConsumersGroupsContainer from 'components/ConsumerGroups/ConsumersGroupsContainer'; import Schemas from 'components/Schemas/Schemas'; import { getClustersReadonlyStatus } from 'redux/reducers/clusters/selectors'; @@ -18,10 +18,7 @@ const Cluster: React.FC = () => { path="/ui/clusters/:clusterName/brokers" component={BrokersContainer} /> - + void; -} - -const Overview: React.FC = ({ - isFetched, - clusterName, - topicName, - partitions, - underReplicatedPartitions, - inSyncReplicas, - replicas, - partitionCount, - internal, - replicationFactor, - segmentSize, - segmentCount, - fetchTopicDetails, -}) => { - React.useEffect(() => { - fetchTopicDetails(clusterName, topicName); - }, [fetchTopicDetails, clusterName, topicName]); - - if (!isFetched) { - return null; - } - - return ( - <> - - {partitionCount} - {replicationFactor} - - {underReplicatedPartitions} - - - {inSyncReplicas} - - {' '} - of - {replicas} - - - - - {internal ? 'Internal' : 'External'} - - - - - - {segmentCount} - -
- - - - - - - - - - - {partitions && - partitions.map(({ partition, leader, offsetMin, offsetMax }) => ( - - - - - - - ))} - -
Partition IDBroker leaderMin offsetMax offset
{partition}{leader}{offsetMin}{offsetMax}
-
- - ); -}; - -export default Overview; diff --git a/kafka-ui-react-app/src/components/Topics/Details/Settings/SettingsEditButton.tsx b/kafka-ui-react-app/src/components/Topics/Details/Settings/SettingsEditButton.tsx deleted file mode 100644 index 9fd4e8298f..0000000000 --- a/kafka-ui-react-app/src/components/Topics/Details/Settings/SettingsEditButton.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; - -interface Props { - to: string; -} - -const SettingsEditButton: React.FC = ({ to }) => ( - - - -); - -export default SettingsEditButton; diff --git a/kafka-ui-react-app/src/components/Topics/Edit/EditContainer.tsx b/kafka-ui-react-app/src/components/Topics/Edit/EditContainer.tsx deleted file mode 100644 index 44db01c5d5..0000000000 --- a/kafka-ui-react-app/src/components/Topics/Edit/EditContainer.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { connect } from 'react-redux'; -import { - RootState, - ClusterName, - TopicName, - Action, - TopicFormDataRaw, -} from 'redux/interfaces'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { - updateTopic, - fetchTopicConfig, - fetchTopicDetails, -} from 'redux/actions'; -import { - getTopicConfigFetched, - getTopicUpdated, - getIsTopicDetailsFetched, - getFullTopic, -} from 'redux/reducers/topics/selectors'; -import { clusterTopicPath } from 'lib/paths'; -import { ThunkDispatch } from 'redux-thunk'; - -import Edit from './Edit'; - -interface RouteProps { - clusterName: ClusterName; - topicName: TopicName; -} - -type OwnProps = RouteComponentProps; - -const mapStateToProps = ( - state: RootState, - { - match: { - params: { topicName, clusterName }, - }, - }: OwnProps -) => ({ - clusterName, - topicName, - topic: getFullTopic(state, topicName), - isFetched: getTopicConfigFetched(state), - isTopicDetailsFetched: getIsTopicDetailsFetched(state), - isTopicUpdated: getTopicUpdated(state), -}); - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, - { history }: OwnProps -) => ({ - fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => - dispatch(fetchTopicDetails(clusterName, topicName)), - fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) => - dispatch(fetchTopicConfig(clusterName, topicName)), - updateTopic: (clusterName: ClusterName, form: TopicFormDataRaw) => - dispatch(updateTopic(clusterName, form)), - redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => { - history.push(clusterTopicPath(clusterName, topicName)); - }, -}); - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit)); diff --git a/kafka-ui-react-app/src/components/Topics/List/List.tsx b/kafka-ui-react-app/src/components/Topics/List/List.tsx index 879c08134a..8a504b4e48 100644 --- a/kafka-ui-react-app/src/components/Topics/List/List.tsx +++ b/kafka-ui-react-app/src/components/Topics/List/List.tsx @@ -1,28 +1,46 @@ import React from 'react'; import { TopicWithDetailedInfo, ClusterName } from 'redux/interfaces'; import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; -import { NavLink } from 'react-router-dom'; +import { NavLink, 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 ListItem from './ListItem'; interface Props { - clusterName: ClusterName; + areTopicsFetching: boolean; topics: TopicWithDetailedInfo[]; externalTopics: TopicWithDetailedInfo[]; + fetchTopicsList(props: FetchTopicsListParams): void; } -const List: React.FC = ({ clusterName, topics, externalTopics }) => { +const List: React.FC = ({ + areTopicsFetching, + topics, + externalTopics, + fetchTopicsList, +}) => { + const { isReadOnly } = React.useContext(ClusterContext); + const { clusterName } = useParams<{ clusterName: ClusterName }>(); + const { page, perPage } = usePagination(); + + React.useEffect(() => { + fetchTopicsList({ clusterName, page, perPage }); + }, [fetchTopicsList, clusterName, page, perPage]); + const [showInternal, setShowInternal] = React.useState(true); - const handleSwitch = () => setShowInternal(!showInternal); - const { isReadOnly } = React.useContext(ClusterContext); + const handleSwitch = React.useCallback(() => { + setShowInternal(!showInternal); + }, [showInternal]); + const items = showInternal ? topics : externalTopics; return (
- All Topics - + {showInternal ? `All Topics` : `External Topics`}
@@ -50,23 +68,33 @@ const List: React.FC = ({ clusterName, topics, externalTopics }) => {
-
- - - - - - - - - - - {items.map((topic) => ( - - ))} - -
Topic NameTotal PartitionsOut of sync replicasType
-
+ {areTopicsFetching ? ( + + ) : ( +
+ + + + + + + + + + + {items.length > 0 ? ( + items.map((topic) => ( + + )) + ) : ( + + + + )} + +
Topic NameTotal PartitionsOut of sync replicasType
No topics found
+
+ )}
); }; diff --git a/kafka-ui-react-app/src/components/Topics/List/ListContainer.ts b/kafka-ui-react-app/src/components/Topics/List/ListContainer.ts index 3dde5c455e..4e54baa125 100644 --- a/kafka-ui-react-app/src/components/Topics/List/ListContainer.ts +++ b/kafka-ui-react-app/src/components/Topics/List/ListContainer.ts @@ -1,29 +1,21 @@ import { connect } from 'react-redux'; -import { ClusterName, RootState } from 'redux/interfaces'; +import { RootState } from 'redux/interfaces'; +import { fetchTopicsList } from 'redux/actions'; import { getTopicList, getExternalTopicList, + getAreTopicsFetching, } from 'redux/reducers/topics/selectors'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; import List from './List'; -interface RouteProps { - clusterName: ClusterName; -} - -type OwnProps = RouteComponentProps; - -const mapStateToProps = ( - state: RootState, - { - match: { - params: { clusterName }, - }, - }: OwnProps -) => ({ - clusterName, +const mapStateToProps = (state: RootState) => ({ + areTopicsFetching: getAreTopicsFetching(state), topics: getTopicList(state), externalTopics: getExternalTopicList(state), }); -export default withRouter(connect(mapStateToProps)(List)); +const mapDispatchToProps = { + fetchTopicsList, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(List); diff --git a/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx b/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx index 0e857e276b..3d8f185b58 100644 --- a/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx @@ -1,22 +1,43 @@ import { mount } from 'enzyme'; import React from 'react'; +import { StaticRouter } from 'react-router-dom'; import ClusterContext from 'components/contexts/ClusterContext'; import List from '../List'; describe('List', () => { describe('when it has readonly flag', () => { it('does not render the Add a Topic button', () => { - const props = { - clusterName: 'Cluster', - topics: [], - externalTopics: [], - }; const component = mount( - - - + + + + + ); expect(component.exists('NavLink')).toBeFalsy(); }); }); + + describe('when it does not have readonly flag', () => { + it('renders the Add a Topic button', () => { + const component = mount( + + + + + + ); + expect(component.exists('NavLink')).toBeTruthy(); + }); + }); }); diff --git a/kafka-ui-react-app/src/components/Topics/Details/Details.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx similarity index 54% rename from kafka-ui-react-app/src/components/Topics/Details/Details.tsx rename to kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx index 796be2c6f9..099c255ba4 100644 --- a/kafka-ui-react-app/src/components/Topics/Details/Details.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx @@ -1,10 +1,8 @@ import React from 'react'; import { ClusterName, TopicName } from 'redux/interfaces'; import { Topic, TopicDetails } from 'generated-sources'; -import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; -import { NavLink, Switch, Route } from 'react-router-dom'; +import { NavLink, Switch, Route, Link } from 'react-router-dom'; import { - clusterTopicsPath, clusterTopicSettingsPath, clusterTopicPath, clusterTopicMessagesPath, @@ -14,7 +12,6 @@ import ClusterContext from 'components/contexts/ClusterContext'; import OverviewContainer from './Overview/OverviewContainer'; import MessagesContainer from './Messages/MessagesContainer'; import SettingsContainer from './Settings/SettingsContainer'; -import SettingsEditButton from './Settings/SettingsEditButton'; interface Props extends Topic, TopicDetails { clusterName: ClusterName; @@ -23,27 +20,11 @@ interface Props extends Topic, TopicDetails { const Details: React.FC = ({ clusterName, topicName }) => { const { isReadOnly } = React.useContext(ClusterContext); - return ( -
-
-
- - {topicName} - -
- {!isReadOnly && ( - - )} -
-
-
+
+ {!isReadOnly && ( + + Edit settings + + )} +
+ +
+ + + + +
); }; diff --git a/kafka-ui-react-app/src/components/Topics/Details/DetailsContainer.ts b/kafka-ui-react-app/src/components/Topics/Topic/Details/DetailsContainer.ts similarity index 100% rename from kafka-ui-react-app/src/components/Topics/Details/DetailsContainer.ts rename to kafka-ui-react-app/src/components/Topics/Topic/Details/DetailsContainer.ts diff --git a/kafka-ui-react-app/src/components/Topics/Details/Messages/MessageItem.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessageItem.tsx similarity index 100% rename from kafka-ui-react-app/src/components/Topics/Details/Messages/MessageItem.tsx rename to kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessageItem.tsx diff --git a/kafka-ui-react-app/src/components/Topics/Details/Messages/Messages.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Messages.tsx similarity index 100% rename from kafka-ui-react-app/src/components/Topics/Details/Messages/Messages.tsx rename to kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Messages.tsx diff --git a/kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesContainer.ts b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessagesContainer.ts similarity index 100% rename from kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesContainer.ts rename to kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessagesContainer.ts diff --git a/kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesTable.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessagesTable.tsx similarity index 100% rename from kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesTable.tsx rename to kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessagesTable.tsx diff --git a/kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/MessageItem.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessageItem.spec.tsx similarity index 93% rename from kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/MessageItem.spec.tsx rename to kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessageItem.spec.tsx index 88a9c01b6f..715d6a8e16 100644 --- a/kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/MessageItem.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessageItem.spec.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import MessageItem from 'components/Topics/Details/Messages/MessageItem'; +import MessageItem from 'components/Topics/Topic/Details/Messages/MessageItem'; import { messages } from './fixtures'; jest.mock('date-fns', () => ({ diff --git a/kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/Messages.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Messages.spec.tsx similarity index 96% rename from kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/Messages.spec.tsx rename to kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Messages.spec.tsx index 8f6f330247..a237ec75ed 100644 --- a/kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/Messages.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Messages.spec.tsx @@ -3,8 +3,10 @@ import { Provider } from 'react-redux'; import { mount, shallow } from 'enzyme'; import * as useDebounce from 'use-debounce'; import DatePicker from 'react-datepicker'; -import Messages, { Props } from 'components/Topics/Details/Messages/Messages'; -import MessagesContainer from 'components/Topics/Details/Messages/MessagesContainer'; +import Messages, { + Props, +} from 'components/Topics/Topic/Details/Messages/Messages'; +import MessagesContainer from 'components/Topics/Topic/Details/Messages/MessagesContainer'; import PageLoader from 'components/common/PageLoader/PageLoader'; import configureStore from 'redux/store/configureStore'; diff --git a/kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/MessagesTable.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx similarity index 95% rename from kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/MessagesTable.spec.tsx rename to kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx index c40a5725f6..691f069e23 100644 --- a/kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/MessagesTable.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import MessagesTable, { MessagesTableProp, -} from 'components/Topics/Details/Messages/MessagesTable'; +} from 'components/Topics/Topic/Details/Messages/MessagesTable'; import { messages } from './fixtures'; jest.mock('date-fns', () => ({ diff --git a/kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/__snapshots__/MessageItem.spec.tsx.snap b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/__snapshots__/MessageItem.spec.tsx.snap similarity index 100% rename from kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/__snapshots__/MessageItem.spec.tsx.snap rename to kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/__snapshots__/MessageItem.spec.tsx.snap diff --git a/kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/__snapshots__/MessagesTable.spec.tsx.snap b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/__snapshots__/MessagesTable.spec.tsx.snap similarity index 100% rename from kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/__snapshots__/MessagesTable.spec.tsx.snap rename to kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/__snapshots__/MessagesTable.spec.tsx.snap diff --git a/kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/fixtures.ts b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/fixtures.ts similarity index 100% rename from kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/fixtures.ts rename to kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/fixtures.ts diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx new file mode 100644 index 0000000000..ce2d79e5de --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { Topic, TopicDetails } from 'generated-sources'; +import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper'; +import Indicator from 'components/common/Dashboard/Indicator'; +import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; + +interface Props extends Topic, TopicDetails {} + +const Overview: React.FC = ({ + partitions, + underReplicatedPartitions, + inSyncReplicas, + replicas, + partitionCount, + internal, + replicationFactor, + segmentSize, + segmentCount, +}) => ( + <> + + {partitionCount} + {replicationFactor} + + {underReplicatedPartitions} + + + {inSyncReplicas} + + {' '} + of + {replicas} + + + + + {internal ? 'Internal' : 'External'} + + + + + + {segmentCount} + +
+ + + + + + + + + + + {partitions?.map(({ partition, leader, offsetMin, offsetMax }) => ( + + + + + + + ))} + +
Partition IDBroker leaderMin offsetMax offset
{partition}{leader}{offsetMin}{offsetMax}
+
+ +); + +export default Overview; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/OverviewContainer.ts b/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/OverviewContainer.ts new file mode 100644 index 0000000000..45abc2aaf4 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/OverviewContainer.ts @@ -0,0 +1,25 @@ +import { connect } from 'react-redux'; +import { RootState, TopicName, ClusterName } from 'redux/interfaces'; +import { getTopicByName } from 'redux/reducers/topics/selectors'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; +import Overview from './Overview'; + +interface RouteProps { + clusterName: ClusterName; + topicName: TopicName; +} + +type OwnProps = RouteComponentProps; + +const mapStateToProps = ( + state: RootState, + { + match: { + params: { topicName }, + }, + }: OwnProps +) => ({ + ...getTopicByName(state, topicName), +}); + +export default withRouter(connect(mapStateToProps)(Overview)); diff --git a/kafka-ui-react-app/src/components/Topics/Details/Settings/Settings.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/Settings.tsx similarity index 89% rename from kafka-ui-react-app/src/components/Topics/Details/Settings/Settings.tsx rename to kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/Settings.tsx index 3cf6c1680e..fe9ff8e689 100644 --- a/kafka-ui-react-app/src/components/Topics/Details/Settings/Settings.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/Settings.tsx @@ -1,5 +1,6 @@ +import { TopicConfig } from 'generated-sources'; import React from 'react'; -import { ClusterName, TopicName, TopicConfig } from 'redux/interfaces'; +import { ClusterName, TopicName } from 'redux/interfaces'; interface Props { clusterName: ClusterName; @@ -56,7 +57,7 @@ const Sertings: React.FC = ({ {config.map((item) => ( - + ))} diff --git a/kafka-ui-react-app/src/components/Topics/Details/Settings/SettingsContainer.ts b/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/SettingsContainer.ts similarity index 100% rename from kafka-ui-react-app/src/components/Topics/Details/Settings/SettingsContainer.ts rename to kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/SettingsContainer.ts diff --git a/kafka-ui-react-app/src/components/Topics/Edit/Edit.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx similarity index 65% rename from kafka-ui-react-app/src/components/Topics/Edit/Edit.tsx rename to kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx index eff06ca29a..b52735e958 100644 --- a/kafka-ui-react-app/src/components/Topics/Edit/Edit.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx @@ -10,22 +10,18 @@ import { import { TopicConfig } from 'generated-sources'; import { useForm, FormProvider } from 'react-hook-form'; import { camelCase } from 'lodash'; - -import TopicForm from '../shared/Form/TopicForm'; -import FormBreadcrumbs from '../shared/Form/FormBreadcrumbs'; +import TopicForm from 'components/Topics/shared/Form/TopicForm'; +import { clusterTopicPath } from 'lib/paths'; +import { useHistory } from 'react-router'; interface Props { clusterName: ClusterName; topicName: TopicName; topic?: TopicWithDetailedInfo; isFetched: boolean; - isTopicDetailsFetched: boolean; isTopicUpdated: boolean; - fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => void; fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) => void; updateTopic: (clusterName: ClusterName, form: TopicFormDataRaw) => void; - redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => void; - resetUploadedState: () => void; } const DEFAULTS = { @@ -68,32 +64,29 @@ const Edit: React.FC = ({ topicName, topic, isFetched, - isTopicDetailsFetched, isTopicUpdated, - fetchTopicDetails, fetchTopicConfig, updateTopic, - redirectToTopicPath, }) => { const defaultValues = topicParams(topic); const methods = useForm({ defaultValues }); const [isSubmitting, setIsSubmitting] = React.useState(false); + const history = useHistory(); React.useEffect(() => { fetchTopicConfig(clusterName, topicName); - fetchTopicDetails(clusterName, topicName); - }, [fetchTopicConfig, fetchTopicDetails, clusterName, topicName]); + }, [fetchTopicConfig, clusterName, topicName]); React.useEffect(() => { if (isSubmitting && isTopicUpdated) { const { name } = methods.getValues(); - redirectToTopicPath(clusterName, name); + history.push(clusterTopicPath(clusterName, name)); } - }, [isSubmitting, isTopicUpdated, redirectToTopicPath, clusterName, methods]); + }, [isSubmitting, isTopicUpdated, clusterTopicPath, clusterName, methods]); - if (!isFetched || !isTopicDetailsFetched || !topic || !topic.config) { + if (!isFetched || !topic || !topic.config) { return null; } @@ -116,27 +109,17 @@ const Edit: React.FC = ({ }; return ( -
-
- + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + + -
- -
- {/* eslint-disable-next-line react/jsx-props-no-spreading */} - - - -
+
); }; diff --git a/kafka-ui-react-app/src/components/Topics/Details/Overview/OverviewContainer.ts b/kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx similarity index 50% rename from kafka-ui-react-app/src/components/Topics/Details/Overview/OverviewContainer.ts rename to kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx index 65c1f5c801..26b5aca48a 100644 --- a/kafka-ui-react-app/src/components/Topics/Details/Overview/OverviewContainer.ts +++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx @@ -1,12 +1,14 @@ import { connect } from 'react-redux'; -import { fetchTopicDetails } from 'redux/actions'; -import { RootState, TopicName, ClusterName } from 'redux/interfaces'; -import { - getTopicByName, - getIsTopicDetailsFetched, -} from 'redux/reducers/topics/selectors'; +import { RootState, ClusterName, TopicName } from 'redux/interfaces'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import Overview from './Overview'; +import { updateTopic, fetchTopicConfig } from 'redux/actions'; +import { + getTopicConfigFetched, + getTopicUpdated, + getFullTopic, +} from 'redux/reducers/topics/selectors'; + +import Edit from './Edit'; interface RouteProps { clusterName: ClusterName; @@ -25,15 +27,14 @@ const mapStateToProps = ( ) => ({ clusterName, topicName, - isFetched: getIsTopicDetailsFetched(state), - ...getTopicByName(state, topicName), + topic: getFullTopic(state, topicName), + isFetched: getTopicConfigFetched(state), + isTopicUpdated: getTopicUpdated(state), }); const mapDispatchToProps = { - fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => - fetchTopicDetails(clusterName, topicName), + fetchTopicConfig, + updateTopic, }; -export default withRouter( - connect(mapStateToProps, mapDispatchToProps)(Overview) -); +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit)); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx new file mode 100644 index 0000000000..008e1f09b7 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { Switch, Route, useParams } from 'react-router-dom'; +import { clusterTopicPath, clusterTopicsPath } from 'lib/paths'; +import { ClusterName, TopicName } from 'redux/interfaces'; +import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; +import EditContainer from 'components/Topics/Topic/Edit/EditContainer'; +import DetailsContainer from 'components/Topics/Topic/Details/DetailsContainer'; +import PageLoader from 'components/common/PageLoader/PageLoader'; + +interface RouterParams { + clusterName: ClusterName; + topicName: TopicName; +} + +interface TopicProps { + isTopicFetching: boolean; + fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => void; +} + +const Topic: React.FC = ({ + isTopicFetching, + fetchTopicDetails, +}) => { + const { clusterName, topicName } = useParams(); + + React.useEffect(() => { + fetchTopicDetails(clusterName, topicName); + }, [fetchTopicDetails, clusterName, topicName]); + + const rootBreadcrumbLinks = [ + { + href: clusterTopicsPath(clusterName), + label: 'All Topics', + }, + ]; + + const childBreadcrumbLinks = [ + ...rootBreadcrumbLinks, + { + href: clusterTopicPath(clusterName, topicName), + label: topicName, + }, + ]; + + const topicPageUrl = '/ui/clusters/:clusterName/topics/:topicName'; + + return ( +
+
+
+ + + Edit + + + {topicName} + + +
+
+ {isTopicFetching ? ( + + ) : ( + + + + + )} +
+ ); +}; + +export default Topic; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/TopicContainer.tsx b/kafka-ui-react-app/src/components/Topics/Topic/TopicContainer.tsx new file mode 100644 index 0000000000..55cee070d7 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/TopicContainer.tsx @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import { RootState } from 'redux/interfaces'; +import { fetchTopicDetails } from 'redux/actions'; +import { getIsTopicDetailsFetching } from 'redux/reducers/topics/selectors'; +import Topic from './Topic'; + +const mapStateToProps = (state: RootState) => ({ + isTopicFetching: getIsTopicDetailsFetching(state), +}); + +const mapDispatchToProps = { + fetchTopicDetails, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Topic); diff --git a/kafka-ui-react-app/src/components/Topics/Topics.tsx b/kafka-ui-react-app/src/components/Topics/Topics.tsx index d119abdfc8..3129d5e9df 100644 --- a/kafka-ui-react-app/src/components/Topics/Topics.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topics.tsx @@ -1,54 +1,26 @@ import React from 'react'; -import { ClusterName } from 'redux/interfaces'; import { Switch, Route } from 'react-router-dom'; -import PageLoader from 'components/common/PageLoader/PageLoader'; -import EditContainer from 'components/Topics/Edit/EditContainer'; import ListContainer from './List/ListContainer'; -import DetailsContainer from './Details/DetailsContainer'; +import TopicContainer from './Topic/TopicContainer'; import NewContainer from './New/NewContainer'; -interface Props { - clusterName: ClusterName; - isFetched: boolean; - fetchTopicsList: (clusterName: ClusterName) => void; -} - -const Topics: React.FC = ({ - clusterName, - isFetched, - fetchTopicsList, -}) => { - React.useEffect(() => { - fetchTopicsList(clusterName); - }, [fetchTopicsList, clusterName]); - - if (isFetched) { - return ( - - - - - - - ); - } - - return ; -}; +const Topics: React.FC = () => ( + + + + + +); export default Topics; diff --git a/kafka-ui-react-app/src/components/Topics/TopicsContainer.ts b/kafka-ui-react-app/src/components/Topics/TopicsContainer.ts deleted file mode 100644 index 75714dd5e9..0000000000 --- a/kafka-ui-react-app/src/components/Topics/TopicsContainer.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { connect } from 'react-redux'; -import { fetchTopicsList } from 'redux/actions'; -import { getIsTopicListFetched } from 'redux/reducers/topics/selectors'; -import { RootState, ClusterName } from 'redux/interfaces'; -import { RouteComponentProps } from 'react-router-dom'; -import Topics from './Topics'; - -interface RouteProps { - clusterName: ClusterName; -} - -type OwnProps = RouteComponentProps; - -const mapStateToProps = ( - state: RootState, - { - match: { - params: { clusterName }, - }, - }: OwnProps -) => ({ - isFetched: getIsTopicListFetched(state), - clusterName, -}); - -const mapDispatchToProps = { - fetchTopicsList: (clusterName: ClusterName) => fetchTopicsList(clusterName), -}; - -export default connect(mapStateToProps, mapDispatchToProps)(Topics); diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx index f032598b4f..1a1080a127 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx @@ -52,7 +52,7 @@ const CustomParams: React.FC = ({ isSubmitting, config }) => { ...formCustomParams, byIndex: { ...formCustomParams.byIndex, - [newIndex]: { name: '', value: '', id: v4() }, + [newIndex]: { name: '', value: '' }, }, allIndexes: [newIndex, ...formCustomParams.allIndexes], }); diff --git a/kafka-ui-react-app/src/lib/hooks/usePagination.ts b/kafka-ui-react-app/src/lib/hooks/usePagination.ts new file mode 100644 index 0000000000..1351742609 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/usePagination.ts @@ -0,0 +1,15 @@ +import { useLocation } from 'react-router'; + +const usePagination = () => { + const params = new URLSearchParams(useLocation().search); + + const page = params.get('page'); + const perPage = params.get('perPage'); + + return { + page: page ? Number(page) : undefined, + perPage: perPage ? Number(perPage) : undefined, + }; +}; + +export default usePagination; diff --git a/kafka-ui-react-app/src/redux/actions/actions.ts b/kafka-ui-react-app/src/redux/actions/actions.ts index 4fffc065a7..6cc0ad8845 100644 --- a/kafka-ui-react-app/src/redux/actions/actions.ts +++ b/kafka-ui-react-app/src/redux/actions/actions.ts @@ -1,5 +1,5 @@ import { createAsyncAction } from 'typesafe-actions'; -import { TopicName, ConsumerGroupID } from 'redux/interfaces'; +import { TopicName, ConsumerGroupID, TopicsState } from 'redux/interfaces'; import { Cluster, @@ -50,7 +50,7 @@ export const fetchTopicsListAction = createAsyncAction( 'GET_TOPICS__REQUEST', 'GET_TOPICS__SUCCESS', 'GET_TOPICS__FAILURE' -)(); +)(); export const fetchTopicMessagesAction = createAsyncAction( 'GET_TOPIC_MESSAGES__REQUEST', diff --git a/kafka-ui-react-app/src/redux/actions/thunks.ts b/kafka-ui-react-app/src/redux/actions/thunks.ts deleted file mode 100644 index 1c49beeb04..0000000000 --- a/kafka-ui-react-app/src/redux/actions/thunks.ts +++ /dev/null @@ -1,318 +0,0 @@ -import { - ClustersApi, - BrokersApi, - TopicsApi, - ConsumerGroupsApi, - SchemasApi, - MessagesApi, - Configuration, - Cluster, - Topic, - TopicFormData, - TopicConfig, - NewSchemaSubject, - SchemaSubject, -} from 'generated-sources'; -import { - ConsumerGroupID, - PromiseThunkResult, - ClusterName, - BrokerId, - TopicName, - TopicMessageQueryParams, - TopicFormFormattedParams, - TopicFormDataRaw, - SchemaName, -} from 'redux/interfaces'; - -import { BASE_PARAMS } from 'lib/constants'; -import * as actions from './actions'; - -const apiClientConf = new Configuration(BASE_PARAMS); -export const clustersApiClient = new ClustersApi(apiClientConf); -export const brokersApiClient = new BrokersApi(apiClientConf); -export const topicsApiClient = new TopicsApi(apiClientConf); -export const consumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf); -export const schemasApiClient = new SchemasApi(apiClientConf); -export const messagesApiClient = new MessagesApi(apiClientConf); - -export const fetchClustersList = (): PromiseThunkResult => async (dispatch) => { - dispatch(actions.fetchClusterListAction.request()); - try { - const clusters: Cluster[] = await clustersApiClient.getClusters(); - dispatch(actions.fetchClusterListAction.success(clusters)); - } catch (e) { - dispatch(actions.fetchClusterListAction.failure()); - } -}; - -export const fetchClusterStats = ( - clusterName: ClusterName -): PromiseThunkResult => async (dispatch) => { - dispatch(actions.fetchClusterStatsAction.request()); - try { - const payload = await clustersApiClient.getClusterStats({ clusterName }); - dispatch(actions.fetchClusterStatsAction.success(payload)); - } catch (e) { - dispatch(actions.fetchClusterStatsAction.failure()); - } -}; - -export const fetchClusterMetrics = ( - clusterName: ClusterName -): PromiseThunkResult => async (dispatch) => { - dispatch(actions.fetchClusterMetricsAction.request()); - try { - const payload = await clustersApiClient.getClusterMetrics({ clusterName }); - dispatch(actions.fetchClusterMetricsAction.success(payload)); - } catch (e) { - dispatch(actions.fetchClusterMetricsAction.failure()); - } -}; - -export const fetchBrokers = ( - clusterName: ClusterName -): PromiseThunkResult => async (dispatch) => { - dispatch(actions.fetchBrokersAction.request()); - try { - const payload = await brokersApiClient.getBrokers({ clusterName }); - dispatch(actions.fetchBrokersAction.success(payload)); - } catch (e) { - dispatch(actions.fetchBrokersAction.failure()); - } -}; - -export const fetchBrokerMetrics = ( - clusterName: ClusterName, - brokerId: BrokerId -): PromiseThunkResult => async (dispatch) => { - dispatch(actions.fetchBrokerMetricsAction.request()); - try { - const payload = await brokersApiClient.getBrokersMetrics({ - clusterName, - id: brokerId, - }); - dispatch(actions.fetchBrokerMetricsAction.success(payload)); - } catch (e) { - dispatch(actions.fetchBrokerMetricsAction.failure()); - } -}; - -export const fetchTopicsList = ( - clusterName: ClusterName -): PromiseThunkResult => async (dispatch) => { - dispatch(actions.fetchTopicsListAction.request()); - try { - const topics = await topicsApiClient.getTopics({ clusterName }); - dispatch(actions.fetchTopicsListAction.success(topics.topics || [])); - } catch (e) { - dispatch(actions.fetchTopicsListAction.failure()); - } -}; - -export const fetchTopicMessages = ( - clusterName: ClusterName, - topicName: TopicName, - queryParams: Partial -): PromiseThunkResult => async (dispatch) => { - dispatch(actions.fetchTopicMessagesAction.request()); - try { - const messages = await messagesApiClient.getTopicMessages({ - clusterName, - topicName, - ...queryParams, - }); - dispatch(actions.fetchTopicMessagesAction.success(messages)); - } catch (e) { - dispatch(actions.fetchTopicMessagesAction.failure()); - } -}; - -export const fetchTopicDetails = ( - clusterName: ClusterName, - topicName: TopicName -): PromiseThunkResult => async (dispatch) => { - dispatch(actions.fetchTopicDetailsAction.request()); - try { - const topicDetails = await topicsApiClient.getTopicDetails({ - clusterName, - topicName, - }); - dispatch( - actions.fetchTopicDetailsAction.success({ - topicName, - details: topicDetails, - }) - ); - } catch (e) { - dispatch(actions.fetchTopicDetailsAction.failure()); - } -}; - -export const fetchTopicConfig = ( - clusterName: ClusterName, - topicName: TopicName -): PromiseThunkResult => async (dispatch) => { - dispatch(actions.fetchTopicConfigAction.request()); - try { - const config = await topicsApiClient.getTopicConfigs({ - clusterName, - topicName, - }); - dispatch(actions.fetchTopicConfigAction.success({ topicName, config })); - } catch (e) { - dispatch(actions.fetchTopicConfigAction.failure()); - } -}; - -const formatTopicFormData = (form: TopicFormDataRaw): TopicFormData => { - const { - name, - partitions, - replicationFactor, - cleanupPolicy, - retentionBytes, - retentionMs, - maxMessageBytes, - minInSyncReplicas, - customParams, - } = form; - - return { - name, - partitions, - replicationFactor, - configs: { - 'cleanup.policy': cleanupPolicy, - 'retention.ms': retentionMs, - 'retention.bytes': retentionBytes, - 'max.message.bytes': maxMessageBytes, - 'min.insync.replicas': minInSyncReplicas, - ...Object.values(customParams || {}).reduce( - (result: TopicFormFormattedParams, customParam: TopicConfig) => { - return { - ...result, - [customParam.name]: customParam.value, - }; - }, - {} - ), - }, - }; -}; - -export const createTopic = ( - clusterName: ClusterName, - form: TopicFormDataRaw -): PromiseThunkResult => async (dispatch) => { - dispatch(actions.createTopicAction.request()); - try { - const topic: Topic = await topicsApiClient.createTopic({ - clusterName, - topicFormData: formatTopicFormData(form), - }); - dispatch(actions.createTopicAction.success(topic)); - } catch (e) { - dispatch(actions.createTopicAction.failure()); - } -}; - -export const updateTopic = ( - clusterName: ClusterName, - form: TopicFormDataRaw -): PromiseThunkResult => async (dispatch) => { - dispatch(actions.updateTopicAction.request()); - try { - const topic: Topic = await topicsApiClient.updateTopic({ - clusterName, - topicName: form.name, - topicFormData: formatTopicFormData(form), - }); - dispatch(actions.updateTopicAction.success(topic)); - } catch (e) { - dispatch(actions.updateTopicAction.failure()); - } -}; - -export const fetchConsumerGroupsList = ( - clusterName: ClusterName -): PromiseThunkResult => async (dispatch) => { - dispatch(actions.fetchConsumerGroupsAction.request()); - try { - const consumerGroups = await consumerGroupsApiClient.getConsumerGroups({ - clusterName, - }); - dispatch(actions.fetchConsumerGroupsAction.success(consumerGroups)); - } catch (e) { - dispatch(actions.fetchConsumerGroupsAction.failure()); - } -}; - -export const fetchConsumerGroupDetails = ( - clusterName: ClusterName, - consumerGroupID: ConsumerGroupID -): PromiseThunkResult => async (dispatch) => { - dispatch(actions.fetchConsumerGroupDetailsAction.request()); - try { - const consumerGroupDetails = await consumerGroupsApiClient.getConsumerGroup( - { - clusterName, - id: consumerGroupID, - } - ); - dispatch( - actions.fetchConsumerGroupDetailsAction.success({ - consumerGroupID, - details: consumerGroupDetails, - }) - ); - } catch (e) { - dispatch(actions.fetchConsumerGroupDetailsAction.failure()); - } -}; - -export const fetchSchemasByClusterName = ( - clusterName: ClusterName -): PromiseThunkResult => async (dispatch) => { - dispatch(actions.fetchSchemasByClusterNameAction.request()); - try { - const schemas = await schemasApiClient.getSchemas({ clusterName }); - dispatch(actions.fetchSchemasByClusterNameAction.success(schemas)); - } catch (e) { - dispatch(actions.fetchSchemasByClusterNameAction.failure()); - } -}; - -export const fetchSchemaVersions = ( - clusterName: ClusterName, - subject: SchemaName -): PromiseThunkResult => async (dispatch) => { - if (!subject) return; - dispatch(actions.fetchSchemaVersionsAction.request()); - try { - const versions = await schemasApiClient.getAllVersionsBySubject({ - clusterName, - subject, - }); - dispatch(actions.fetchSchemaVersionsAction.success(versions)); - } catch (e) { - dispatch(actions.fetchSchemaVersionsAction.failure()); - } -}; - -export const createSchema = ( - clusterName: ClusterName, - newSchemaSubject: NewSchemaSubject -): PromiseThunkResult => async (dispatch) => { - dispatch(actions.createSchemaAction.request()); - try { - const schema: SchemaSubject = await schemasApiClient.createNewSchema({ - clusterName, - newSchemaSubject, - }); - dispatch(actions.createSchemaAction.success(schema)); - } catch (e) { - dispatch(actions.createSchemaAction.failure()); - throw e; - } -}; diff --git a/kafka-ui-react-app/src/redux/actions/thunks/brokers.ts b/kafka-ui-react-app/src/redux/actions/thunks/brokers.ts new file mode 100644 index 0000000000..eb0d9a133b --- /dev/null +++ b/kafka-ui-react-app/src/redux/actions/thunks/brokers.ts @@ -0,0 +1,36 @@ +import { BrokersApi, Configuration } from 'generated-sources'; +import { PromiseThunkResult, ClusterName, BrokerId } from 'redux/interfaces'; + +import { BASE_PARAMS } from 'lib/constants'; +import * as actions from '../actions'; + +const apiClientConf = new Configuration(BASE_PARAMS); +export const brokersApiClient = new BrokersApi(apiClientConf); + +export const fetchBrokers = ( + clusterName: ClusterName +): PromiseThunkResult => async (dispatch) => { + dispatch(actions.fetchBrokersAction.request()); + try { + const payload = await brokersApiClient.getBrokers({ clusterName }); + dispatch(actions.fetchBrokersAction.success(payload)); + } catch (e) { + dispatch(actions.fetchBrokersAction.failure()); + } +}; + +export const fetchBrokerMetrics = ( + clusterName: ClusterName, + brokerId: BrokerId +): PromiseThunkResult => async (dispatch) => { + dispatch(actions.fetchBrokerMetricsAction.request()); + try { + const payload = await brokersApiClient.getBrokersMetrics({ + clusterName, + id: brokerId, + }); + dispatch(actions.fetchBrokerMetricsAction.success(payload)); + } catch (e) { + dispatch(actions.fetchBrokerMetricsAction.failure()); + } +}; diff --git a/kafka-ui-react-app/src/redux/actions/thunks/clusters.ts b/kafka-ui-react-app/src/redux/actions/thunks/clusters.ts new file mode 100644 index 0000000000..0336336806 --- /dev/null +++ b/kafka-ui-react-app/src/redux/actions/thunks/clusters.ts @@ -0,0 +1,42 @@ +import { ClustersApi, Configuration, Cluster } from 'generated-sources'; +import { PromiseThunkResult, ClusterName } from 'redux/interfaces'; + +import { BASE_PARAMS } from 'lib/constants'; +import * as actions from '../actions'; + +const apiClientConf = new Configuration(BASE_PARAMS); +export const clustersApiClient = new ClustersApi(apiClientConf); + +export const fetchClustersList = (): PromiseThunkResult => async (dispatch) => { + dispatch(actions.fetchClusterListAction.request()); + try { + const clusters: Cluster[] = await clustersApiClient.getClusters(); + dispatch(actions.fetchClusterListAction.success(clusters)); + } catch (e) { + dispatch(actions.fetchClusterListAction.failure()); + } +}; + +export const fetchClusterStats = ( + clusterName: ClusterName +): PromiseThunkResult => async (dispatch) => { + dispatch(actions.fetchClusterStatsAction.request()); + try { + const payload = await clustersApiClient.getClusterStats({ clusterName }); + dispatch(actions.fetchClusterStatsAction.success(payload)); + } catch (e) { + dispatch(actions.fetchClusterStatsAction.failure()); + } +}; + +export const fetchClusterMetrics = ( + clusterName: ClusterName +): PromiseThunkResult => async (dispatch) => { + dispatch(actions.fetchClusterMetricsAction.request()); + try { + const payload = await clustersApiClient.getClusterMetrics({ clusterName }); + dispatch(actions.fetchClusterMetricsAction.success(payload)); + } catch (e) { + dispatch(actions.fetchClusterMetricsAction.failure()); + } +}; diff --git a/kafka-ui-react-app/src/redux/actions/thunks/consumerGroups.ts b/kafka-ui-react-app/src/redux/actions/thunks/consumerGroups.ts new file mode 100644 index 0000000000..cddbfd7d55 --- /dev/null +++ b/kafka-ui-react-app/src/redux/actions/thunks/consumerGroups.ts @@ -0,0 +1,49 @@ +import { ConsumerGroupsApi, Configuration } from 'generated-sources'; +import { + ConsumerGroupID, + PromiseThunkResult, + ClusterName, +} from 'redux/interfaces'; + +import { BASE_PARAMS } from 'lib/constants'; +import * as actions from '../actions'; + +const apiClientConf = new Configuration(BASE_PARAMS); +export const consumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf); + +export const fetchConsumerGroupsList = ( + clusterName: ClusterName +): PromiseThunkResult => async (dispatch) => { + dispatch(actions.fetchConsumerGroupsAction.request()); + try { + const consumerGroups = await consumerGroupsApiClient.getConsumerGroups({ + clusterName, + }); + dispatch(actions.fetchConsumerGroupsAction.success(consumerGroups)); + } catch (e) { + dispatch(actions.fetchConsumerGroupsAction.failure()); + } +}; + +export const fetchConsumerGroupDetails = ( + clusterName: ClusterName, + consumerGroupID: ConsumerGroupID +): PromiseThunkResult => async (dispatch) => { + dispatch(actions.fetchConsumerGroupDetailsAction.request()); + try { + const consumerGroupDetails = await consumerGroupsApiClient.getConsumerGroup( + { + clusterName, + id: consumerGroupID, + } + ); + dispatch( + actions.fetchConsumerGroupDetailsAction.success({ + consumerGroupID, + details: consumerGroupDetails, + }) + ); + } catch (e) { + dispatch(actions.fetchConsumerGroupDetailsAction.failure()); + } +}; diff --git a/kafka-ui-react-app/src/redux/actions/thunks/index.ts b/kafka-ui-react-app/src/redux/actions/thunks/index.ts new file mode 100644 index 0000000000..2eadadd730 --- /dev/null +++ b/kafka-ui-react-app/src/redux/actions/thunks/index.ts @@ -0,0 +1,5 @@ +export * from './brokers'; +export * from './clusters'; +export * from './consumerGroups'; +export * from './schemas'; +export * from './topics'; diff --git a/kafka-ui-react-app/src/redux/actions/thunks/schemas.ts b/kafka-ui-react-app/src/redux/actions/thunks/schemas.ts new file mode 100644 index 0000000000..d3223e5835 --- /dev/null +++ b/kafka-ui-react-app/src/redux/actions/thunks/schemas.ts @@ -0,0 +1,59 @@ +import { + SchemasApi, + Configuration, + NewSchemaSubject, + SchemaSubject, +} from 'generated-sources'; +import { PromiseThunkResult, ClusterName, SchemaName } from 'redux/interfaces'; + +import { BASE_PARAMS } from 'lib/constants'; +import * as actions from '../actions'; + +const apiClientConf = new Configuration(BASE_PARAMS); +export const schemasApiClient = new SchemasApi(apiClientConf); + +export const fetchSchemasByClusterName = ( + clusterName: ClusterName +): PromiseThunkResult => async (dispatch) => { + dispatch(actions.fetchSchemasByClusterNameAction.request()); + try { + const schemas = await schemasApiClient.getSchemas({ clusterName }); + dispatch(actions.fetchSchemasByClusterNameAction.success(schemas)); + } catch (e) { + dispatch(actions.fetchSchemasByClusterNameAction.failure()); + } +}; + +export const fetchSchemaVersions = ( + clusterName: ClusterName, + subject: SchemaName +): PromiseThunkResult => async (dispatch) => { + if (!subject) return; + dispatch(actions.fetchSchemaVersionsAction.request()); + try { + const versions = await schemasApiClient.getAllVersionsBySubject({ + clusterName, + subject, + }); + dispatch(actions.fetchSchemaVersionsAction.success(versions)); + } catch (e) { + dispatch(actions.fetchSchemaVersionsAction.failure()); + } +}; + +export const createSchema = ( + clusterName: ClusterName, + newSchemaSubject: NewSchemaSubject +): PromiseThunkResult => async (dispatch) => { + dispatch(actions.createSchemaAction.request()); + try { + const schema: SchemaSubject = await schemasApiClient.createNewSchema({ + clusterName, + newSchemaSubject, + }); + dispatch(actions.createSchemaAction.success(schema)); + } catch (e) { + dispatch(actions.createSchemaAction.failure()); + throw e; + } +}; diff --git a/kafka-ui-react-app/src/redux/actions/thunks/topics.ts b/kafka-ui-react-app/src/redux/actions/thunks/topics.ts new file mode 100644 index 0000000000..8bbc56dbeb --- /dev/null +++ b/kafka-ui-react-app/src/redux/actions/thunks/topics.ts @@ -0,0 +1,186 @@ +import { v4 } from 'uuid'; +import { + TopicsApi, + MessagesApi, + Configuration, + Topic, + TopicFormData, + TopicConfig, +} from 'generated-sources'; +import { + PromiseThunkResult, + ClusterName, + TopicName, + TopicMessageQueryParams, + TopicFormFormattedParams, + TopicFormDataRaw, + TopicsState, +} from 'redux/interfaces'; + +import { BASE_PARAMS } from 'lib/constants'; +import * as actions from '../actions'; + +const apiClientConf = new Configuration(BASE_PARAMS); +export const topicsApiClient = new TopicsApi(apiClientConf); +export const messagesApiClient = new MessagesApi(apiClientConf); + +export interface FetchTopicsListParams { + clusterName: ClusterName; + page?: number; + perPage?: number; +} + +export const fetchTopicsList = ( + params: FetchTopicsListParams +): PromiseThunkResult => async (dispatch, getState) => { + dispatch(actions.fetchTopicsListAction.request()); + try { + const { topics, pageCount } = await topicsApiClient.getTopics(params); + const newState = (topics || []).reduce( + (memo: TopicsState, topic) => ({ + ...memo, + byName: { + ...memo.byName, + [topic.name]: { + ...memo.byName[topic.name], + ...topic, + id: v4(), + }, + }, + allNames: [...memo.allNames, topic.name], + }), + { + ...getState().topics, + allNames: [], + totalPages: pageCount || 1, + } + ); + dispatch(actions.fetchTopicsListAction.success(newState)); + } catch (e) { + dispatch(actions.fetchTopicsListAction.failure()); + } +}; + +export const fetchTopicMessages = ( + clusterName: ClusterName, + topicName: TopicName, + queryParams: Partial +): PromiseThunkResult => async (dispatch) => { + dispatch(actions.fetchTopicMessagesAction.request()); + try { + const messages = await messagesApiClient.getTopicMessages({ + clusterName, + topicName, + ...queryParams, + }); + dispatch(actions.fetchTopicMessagesAction.success(messages)); + } catch (e) { + dispatch(actions.fetchTopicMessagesAction.failure()); + } +}; + +export const fetchTopicDetails = ( + clusterName: ClusterName, + topicName: TopicName +): PromiseThunkResult => async (dispatch) => { + dispatch(actions.fetchTopicDetailsAction.request()); + try { + const topicDetails = await topicsApiClient.getTopicDetails({ + clusterName, + topicName, + }); + dispatch( + actions.fetchTopicDetailsAction.success({ + topicName, + details: topicDetails, + }) + ); + } catch (e) { + dispatch(actions.fetchTopicDetailsAction.failure()); + } +}; + +export const fetchTopicConfig = ( + clusterName: ClusterName, + topicName: TopicName +): PromiseThunkResult => async (dispatch) => { + dispatch(actions.fetchTopicConfigAction.request()); + try { + const config = await topicsApiClient.getTopicConfigs({ + clusterName, + topicName, + }); + dispatch(actions.fetchTopicConfigAction.success({ topicName, config })); + } catch (e) { + dispatch(actions.fetchTopicConfigAction.failure()); + } +}; + +const formatTopicFormData = (form: TopicFormDataRaw): TopicFormData => { + const { + name, + partitions, + replicationFactor, + cleanupPolicy, + retentionBytes, + retentionMs, + maxMessageBytes, + minInSyncReplicas, + customParams, + } = form; + + return { + name, + partitions, + replicationFactor, + configs: { + 'cleanup.policy': cleanupPolicy, + 'retention.ms': retentionMs, + 'retention.bytes': retentionBytes, + 'max.message.bytes': maxMessageBytes, + 'min.insync.replicas': minInSyncReplicas, + ...Object.values(customParams || {}).reduce( + (result: TopicFormFormattedParams, customParam: TopicConfig) => { + return { + ...result, + [customParam.name]: customParam.value, + }; + }, + {} + ), + }, + }; +}; + +export const createTopic = ( + clusterName: ClusterName, + form: TopicFormDataRaw +): PromiseThunkResult => async (dispatch) => { + dispatch(actions.createTopicAction.request()); + try { + const topic: Topic = await topicsApiClient.createTopic({ + clusterName, + topicFormData: formatTopicFormData(form), + }); + dispatch(actions.createTopicAction.success(topic)); + } catch (e) { + dispatch(actions.createTopicAction.failure()); + } +}; + +export const updateTopic = ( + clusterName: ClusterName, + form: TopicFormDataRaw +): PromiseThunkResult => async (dispatch) => { + dispatch(actions.updateTopicAction.request()); + try { + const topic: Topic = await topicsApiClient.updateTopic({ + clusterName, + topicName: form.name, + topicFormData: formatTopicFormData(form), + }); + dispatch(actions.updateTopicAction.success(topic)); + } catch (e) { + dispatch(actions.updateTopicAction.failure()); + } +}; diff --git a/kafka-ui-react-app/src/redux/interfaces/topic.ts b/kafka-ui-react-app/src/redux/interfaces/topic.ts index e7b439303a..d7c2ee99fc 100644 --- a/kafka-ui-react-app/src/redux/interfaces/topic.ts +++ b/kafka-ui-react-app/src/redux/interfaces/topic.ts @@ -2,7 +2,7 @@ import { Topic, TopicDetails, TopicMessage, - TopicConfig as InputTopicConfig, + TopicConfig, TopicFormData, GetTopicMessagesRequest, } from 'generated-sources'; @@ -14,10 +14,6 @@ export enum CleanupPolicy { Compact = 'compact', } -export interface TopicConfig extends InputTopicConfig { - id: string; -} - export interface TopicConfigByName { byName: TopicConfigParams; } @@ -50,12 +46,12 @@ export interface TopicFormCustomParams { export interface TopicWithDetailedInfo extends Topic, TopicDetails { config?: TopicConfig[]; - id: string; } export interface TopicsState { byName: { [topicName: string]: TopicWithDetailedInfo }; allNames: TopicName[]; + totalPages: number; messages: TopicMessage[]; } diff --git a/kafka-ui-react-app/src/redux/reducers/topics/reducer.ts b/kafka-ui-react-app/src/redux/reducers/topics/reducer.ts index 61e58772cc..bba97d142a 100644 --- a/kafka-ui-react-app/src/redux/reducers/topics/reducer.ts +++ b/kafka-ui-react-app/src/redux/reducers/topics/reducer.ts @@ -1,4 +1,3 @@ -import { v4 } from 'uuid'; import { Topic, TopicMessage } from 'generated-sources'; import { Action, TopicsState } from 'redux/interfaces'; import { getType } from 'typesafe-actions'; @@ -7,38 +6,16 @@ import * as actions from 'redux/actions'; export const initialState: TopicsState = { byName: {}, allNames: [], + totalPages: 1, messages: [], }; -const updateTopicList = (state: TopicsState, payload: Topic[]): TopicsState => { - const initialMemo: TopicsState = { - ...state, - allNames: [], - }; - - return payload.reduce( - (memo: TopicsState, topic) => ({ - ...memo, - byName: { - ...memo.byName, - [topic.name]: { - ...memo.byName[topic.name], - ...topic, - id: v4(), - }, - }, - allNames: [...memo.allNames, topic.name], - }), - initialMemo - ); -}; - const addToTopicList = (state: TopicsState, payload: Topic): TopicsState => { const newState: TopicsState = { ...state, }; newState.allNames.push(payload.name); - newState.byName[payload.name] = { ...payload, id: v4() }; + newState.byName[payload.name] = { ...payload }; return newState; }; @@ -70,7 +47,7 @@ const transformTopicMessages = ( const reducer = (state = initialState, action: Action): TopicsState => { switch (action.type) { case getType(actions.fetchTopicsListAction.success): - return updateTopicList(state, action.payload); + return action.payload; case getType(actions.fetchTopicDetailsAction.success): return { ...state, @@ -93,7 +70,6 @@ const reducer = (state = initialState, action: Action): TopicsState => { ...state.byName[action.payload.topicName], config: action.payload.config.map((inputConfig) => ({ ...inputConfig, - id: v4(), })), }, }, diff --git a/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts b/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts index b767c80432..5271ecd4e6 100644 --- a/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts +++ b/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts @@ -26,11 +26,21 @@ const getTopicConfigFetchingStatus = createFetchingSelector('GET_TOPIC_CONFIG'); const getTopicCreationStatus = createFetchingSelector('POST_TOPIC'); const getTopicUpdateStatus = createFetchingSelector('PATCH_TOPIC'); -export const getIsTopicListFetched = createSelector( +export const getAreTopicsFetching = createSelector( + getTopicListFetchingStatus, + (status) => status === 'fetching' || status === 'notFetched' +); + +export const getAreTopicsFetched = createSelector( getTopicListFetchingStatus, (status) => status === 'fetched' ); +export const getIsTopicDetailsFetching = createSelector( + getTopicDetailsFetchingStatus, + (status) => status === 'notFetched' || status === 'fetching' +); + export const getIsTopicDetailsFetched = createSelector( getTopicDetailsFetchingStatus, (status) => status === 'fetched' @@ -57,7 +67,7 @@ export const getTopicUpdated = createSelector( ); export const getTopicList = createSelector( - getIsTopicListFetched, + getAreTopicsFetched, getAllNames, getTopicMap, (isFetched, allNames, byName) => {