Explorar o código

Added concumer groups list with search.

Sofia Shnaidman %!s(int64=5) %!d(string=hai) anos
pai
achega
5f6b467c2b

+ 2 - 0
kafka-ui-react-app/mock/index.js

@@ -5,6 +5,7 @@ const brokerMetrics = require('./payload/brokerMetrics.json');
 const topics = require('./payload/topics.json');
 const topicDetails = require('./payload/topicDetails.json');
 const topicConfigs = require('./payload/topicConfigs.json');
+const consumerGroups = require('./payload/consumerGroups.json');
 
 const db = {
     clusters,
@@ -13,6 +14,7 @@ const db = {
     topics: topics.map((topic) => ({...topic, id: topic.name})),
     topicDetails,
     topicConfigs,
+    consumerGroups: consumerGroups.map((group) => ({...group, id: group.consumerGroupId}))
 };
 const server = jsonServer.create();
 const router = jsonServer.router(db);

+ 39 - 0
kafka-ui-react-app/mock/payload/consumerGroups.json

@@ -0,0 +1,39 @@
+[
+  {
+    "clusterId": "fake.cluster",
+    "consumerGroupId": "_fake.cluster.consumer_1",
+    "numConsumers": 1,
+    "numTopics": 11
+  },
+  {
+    "clusterId": "fake.cluster",
+    "consumerGroupId": "_fake.cluster.consumer_2",
+    "numConsumers": 2,
+    "numTopics": 22
+  },
+  {
+    "clusterId": "fake.cluster",
+    "consumerGroupId": "_fake.cluster.consumer_3",
+    "numConsumers": 3,
+    "numTopics": 33
+  },
+
+  {
+    "clusterId": "kafka-ui.cluster",
+    "consumerGroupId": "_kafka-ui.cluster.consumer_1",
+    "numConsumers": 4,
+    "numTopics": 44
+  },
+  {
+    "clusterId": "kafka-ui.cluster",
+    "consumerGroupId": "_kafka-ui.cluster.consumer_2",
+    "numConsumers": 5,
+    "numTopics": 55
+  },
+  {
+    "clusterId": "kafka-ui.cluster",
+    "consumerGroupId": "_kafka-ui.cluster.consumer_3",
+    "numConsumers": 6,
+    "numTopics": 66
+  }
+]

+ 2 - 0
kafka-ui-react-app/src/components/App.tsx

@@ -10,6 +10,7 @@ import TopicsContainer from './Topics/TopicsContainer';
 import NavConatiner from './Nav/NavConatiner';
 import PageLoader from './common/PageLoader/PageLoader';
 import Dashboard from './Dashboard/Dashboard';
+import ConsumersGroupsContainer from './ConsumerGroups/ConsumersGroupsContainer';
 
 interface AppProps {
   isClusterListFetched: boolean;
@@ -39,6 +40,7 @@ const App: React.FC<AppProps> = ({
             <Route exact path="/clusters" component={Dashboard} />
             <Route path="/clusters/:clusterName/topics" component={TopicsContainer} />
             <Route path="/clusters/:clusterName/brokers" component={BrokersContainer} />
+            <Route path="/clusters/:clusterName/consumer-groups" component={ConsumersGroupsContainer} />
             <Redirect from="/clusters/:clusterName" to="/clusters/:clusterName/brokers" />
           </Switch>
         ) : (

+ 34 - 0
kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx

@@ -0,0 +1,34 @@
+import React from 'react';
+import { ClusterName } from 'redux/interfaces';
+import {
+  Switch,
+  Route,
+} from 'react-router-dom';
+import ListContainer from './List/ListContainer';
+import PageLoader from 'components/common/PageLoader/PageLoader';
+
+interface Props {
+  clusterName: ClusterName;
+  isFetched: boolean;
+  fetchConsumerGroupsList: (clusterName: ClusterName) => void;
+}
+
+const ConsumerGroups: React.FC<Props> = ({
+  clusterName,
+  isFetched,
+  fetchConsumerGroupsList,
+}) => {
+  React.useEffect(() => { fetchConsumerGroupsList(clusterName); }, [fetchConsumerGroupsList, clusterName]);
+
+  if (isFetched) {
+    return (
+      <Switch>
+        <Route exact path="/clusters/:clusterName/consumer-groups" component={ListContainer} />
+      </Switch>
+    );
+  }
+
+  return (<PageLoader />);
+};
+
+export default ConsumerGroups;

+ 24 - 0
kafka-ui-react-app/src/components/ConsumerGroups/ConsumersGroupsContainer.ts

@@ -0,0 +1,24 @@
+import { connect } from 'react-redux';
+import { fetchConsumerGroupsList } from 'redux/actions';
+import { RootState, ClusterName } from 'redux/interfaces';
+import { RouteComponentProps } from 'react-router-dom';
+import ConsumerGroups from './ConsumerGroups';
+import { getIsConsumerGroupsListFetched } from '../../redux/reducers/consumerGroups/selectors';
+
+
+interface RouteProps {
+  clusterName: ClusterName;
+}
+
+interface OwnProps extends RouteComponentProps<RouteProps> { }
+
+const mapStateToProps = (state: RootState, { match: { params: { clusterName } }}: OwnProps) => ({
+  isFetched: getIsConsumerGroupsListFetched(state),
+  clusterName,
+});
+
+const mapDispatchToProps = {
+  fetchConsumerGroupsList: (clusterName: ClusterName) => fetchConsumerGroupsList(clusterName),
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(ConsumerGroups);

+ 64 - 0
kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx

@@ -0,0 +1,64 @@
+import React, { ChangeEvent } from 'react';
+import { ConsumerGroup, ClusterName } from 'redux/interfaces';
+import ListItem from './ListItem';
+import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
+
+interface Props {
+  clusterName: ClusterName;
+  consumerGroups: (ConsumerGroup)[];
+}
+
+const List: React.FC<Props> = ({
+  consumerGroups,
+}) => {
+
+  const [searchText, setSearchText] = React.useState<string>('');
+
+  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    setSearchText(event.target.value);
+  };
+
+  const items = consumerGroups;
+
+  return (
+    <div className="section">
+      <Breadcrumb>All Consumer Groups</Breadcrumb>
+
+      <div className="box">
+        <div className="columns">
+          <div className="column is-half is-offset-half">
+            <input  id="searchText"
+                  type="text"
+                  name="searchText"
+                  className="input"
+                  placeholder="Search"
+                  value={searchText}
+                  onChange={handleInputChange}
+                />
+          </div>
+        </div>
+        <table className="table is-striped is-fullwidth">
+          <thead>
+            <tr>
+              <th>Consumer group ID</th>
+              <th>Num of consumers</th>
+              <th>Num of topics</th>
+            </tr>
+          </thead>
+          <tbody>
+            {items
+              .filter( (consumerGroup) => !searchText || consumerGroup?.consumerGroupId?.indexOf(searchText) >= 0)
+              .map((consumerGroup, index) => (
+                <ListItem
+                  key={`consumer-group-list-item-key-${index}`}
+                  {...consumerGroup}
+                />
+            ))}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  );
+};
+
+export default List;

+ 20 - 0
kafka-ui-react-app/src/components/ConsumerGroups/List/ListContainer.ts

@@ -0,0 +1,20 @@
+import { connect } from 'react-redux';
+import {ClusterName, RootState} from 'redux/interfaces';
+import { getConsumerGroupsList } from 'redux/reducers/consumerGroups/selectors';
+import List from './List';
+import { withRouter, RouteComponentProps } from 'react-router-dom';
+
+interface RouteProps {
+  clusterName: ClusterName;
+}
+
+interface OwnProps extends RouteComponentProps<RouteProps> { }
+
+const mapStateToProps = (state: RootState, { match: { params: { clusterName } } }: OwnProps) => ({
+  clusterName,
+  consumerGroups: getConsumerGroupsList(state)
+});
+
+export default withRouter(
+  connect(mapStateToProps)(List)
+);

+ 24 - 0
kafka-ui-react-app/src/components/ConsumerGroups/List/ListItem.tsx

@@ -0,0 +1,24 @@
+import React from 'react';
+import { NavLink } from 'react-router-dom';
+import { ConsumerGroup } from 'redux/interfaces';
+
+const ListItem: React.FC<ConsumerGroup> = ({
+  consumerGroupId,
+  numConsumers,
+  numTopics,
+}) => {
+  return (
+    <tr>
+      {/* <td>
+        <NavLink exact to={`consumer-groups/${consumerGroupId}`} activeClassName="is-active" className="title is-6">
+          {consumerGroupId}
+        </NavLink>
+      </td> */}
+      <td>{consumerGroupId}</td>
+      <td>{numConsumers}</td>
+      <td>{numTopics}</td>
+    </tr>
+  );
+}
+
+export default ListItem;

+ 4 - 1
kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx

@@ -1,7 +1,7 @@
 import React, { CSSProperties } from 'react';
 import { Cluster } from 'redux/interfaces';
 import { NavLink } from 'react-router-dom';
-import { clusterBrokersPath, clusterTopicsPath } from 'lib/paths';
+import { clusterBrokersPath, clusterTopicsPath, clusterConsumerGroupsPath } from 'lib/paths';
 
 interface Props extends Cluster {}
 
@@ -37,6 +37,9 @@ const ClusterMenu: React.FC<Props> = ({
         <NavLink to={clusterTopicsPath(name)} activeClassName="is-active" title="Topics">
           Topics
         </NavLink>
+        <NavLink to={clusterConsumerGroupsPath(name)} activeClassName="is-active" title="Consumers">
+          Consumers
+        </NavLink>
       </ul>
     </li>
   </ul>

+ 2 - 0
kafka-ui-react-app/src/lib/paths.ts

@@ -13,3 +13,5 @@ export const clusterTopicNewPath = (clusterName: ClusterName) => `${clusterPath(
 export const clusterTopicPath = (clusterName: ClusterName, topicName: TopicName) => `${clusterTopicsPath(clusterName)}/${topicName}`;
 export const clusterTopicSettingsPath = (clusterName: ClusterName, topicName: TopicName) => `${clusterTopicsPath(clusterName)}/${topicName}/settings`;
 export const clusterTopicMessagesPath = (clusterName: ClusterName, topicName: TopicName) => `${clusterTopicsPath(clusterName)}/${topicName}/messages`;
+
+export const clusterConsumerGroupsPath = (clusterName: ClusterName) => `${clusterPath(clusterName)}/consumer-groups`;

+ 4 - 0
kafka-ui-react-app/src/redux/actionType.ts

@@ -26,4 +26,8 @@ export enum ActionType {
   POST_TOPIC__REQUEST = 'POST_TOPIC__REQUEST',
   POST_TOPIC__SUCCESS = 'POST_TOPIC__SUCCESS',
   POST_TOPIC__FAILURE = 'POST_TOPIC__FAILURE',
+
+  GET_CONSUMER_GROUPS__REQUEST = 'GET_CONSUMER_GROUPS__REQUEST',
+  GET_CONSUMER_GROUPS__SUCCESS = 'GET_CONSUMER_GROUPS__SUCCESS',
+  GET_CONSUMER_GROUPS__FAILURE = 'GET_CONSUMER_GROUPS__FAILURE',
 };

+ 7 - 0
kafka-ui-react-app/src/redux/actions/actions.ts

@@ -1,5 +1,6 @@
 import { createAsyncAction} from 'typesafe-actions';
 import { ActionType } from 'redux/actionType';
+import { ConsumerGroup } from '../interfaces/consumerGroup';
 import {
   Broker,
   BrokerMetrics,
@@ -51,3 +52,9 @@ export const createTopicAction = createAsyncAction(
   ActionType.POST_TOPIC__SUCCESS,
   ActionType.POST_TOPIC__FAILURE,
 )<undefined, Topic, undefined>();
+
+export const fetchConsumerGroupsAction = createAsyncAction(
+  ActionType.GET_CONSUMER_GROUPS__REQUEST,
+  ActionType.GET_CONSUMER_GROUPS__SUCCESS,
+  ActionType.GET_CONSUMER_GROUPS__FAILURE,
+)<undefined, ConsumerGroup[], undefined>();

+ 10 - 0
kafka-ui-react-app/src/redux/actions/thunks.ts

@@ -77,3 +77,13 @@ export const createTopic = (clusterName: ClusterName, form: TopicFormData): Prom
     dispatch(actions.createTopicAction.failure());
   }
 };
+
+export const fetchConsumerGroupsList = (clusterName: ClusterName): PromiseThunk<void> => async (dispatch) => {
+  dispatch(actions.fetchConsumerGroupsAction.request());
+  try {
+    const consumerGroups = await api.getConsumerGroups(clusterName);
+    dispatch(actions.fetchConsumerGroupsAction.success(consumerGroups));
+  } catch (e) {
+    dispatch(actions.fetchConsumerGroupsAction.failure());
+  }
+};

+ 8 - 0
kafka-ui-react-app/src/redux/api/consumerGroups.ts

@@ -0,0 +1,8 @@
+import { ClusterName } from '../interfaces/cluster';
+import { ConsumerGroup } from '../interfaces/consumerGroup';
+import { BASE_PARAMS, BASE_URL } from '../../lib/constants';
+
+
+export const getConsumerGroups = (clusterName: ClusterName): Promise<ConsumerGroup[]> =>
+  fetch(`${BASE_URL}/clusters/${clusterName}/consumerGroups`, { ...BASE_PARAMS })
+    .then(res => res.json());

+ 1 - 0
kafka-ui-react-app/src/redux/api/index.ts

@@ -1,3 +1,4 @@
 export * from './topics';
 export * from './clusters';
 export * from './brokers';
+export * from './consumerGroups';

+ 5 - 0
kafka-ui-react-app/src/redux/interfaces/consumerGroup.ts

@@ -0,0 +1,5 @@
+export interface ConsumerGroup {
+  consumerGroupId: string;
+  numConsumers: number;
+  numTopics: number;
+}

+ 3 - 0
kafka-ui-react-app/src/redux/interfaces/index.ts

@@ -8,10 +8,12 @@ import { TopicsState } from './topic';
 import { Cluster } from './cluster';
 import { BrokersState } from './broker';
 import { LoaderState } from './loader';
+import { ConsumerGroup } from './consumerGroup';
 
 export * from './topic';
 export * from './cluster';
 export * from './broker';
+export * from './consumerGroup';
 export * from './loader';
 
 export enum FetchStatus {
@@ -25,6 +27,7 @@ export interface RootState {
   topics: TopicsState;
   clusters: Cluster[];
   brokers: BrokersState;
+  consumerGroups: ConsumerGroup[];
   loader: LoaderState;
 }
 

+ 15 - 0
kafka-ui-react-app/src/redux/reducers/consumerGroups/reducer.ts

@@ -0,0 +1,15 @@
+import { Action, ConsumerGroup } from 'redux/interfaces';
+import { ActionType } from 'redux/actionType';
+
+export const initialState: ConsumerGroup[] = [];
+
+const reducer = (state = initialState, action: Action): ConsumerGroup[] => {
+  switch (action.type) {
+    case ActionType.GET_CONSUMER_GROUPS__SUCCESS:
+      return action.payload;
+    default:
+      return state;
+  }
+};
+
+export default reducer;

+ 15 - 0
kafka-ui-react-app/src/redux/reducers/consumerGroups/selectors.ts

@@ -0,0 +1,15 @@
+import { createSelector } from 'reselect';
+import { ConsumerGroup, RootState, FetchStatus } from 'redux/interfaces';
+import { createFetchingSelector } from 'redux/reducers/loader/selectors';
+
+
+const consumerGroupsState = ({ consumerGroups }: RootState): ConsumerGroup[] => consumerGroups;
+
+const getConsumerGroupsListFetchingStatus = createFetchingSelector('GET_CONSUMER_GROUPS');
+
+export const getIsConsumerGroupsListFetched = createSelector(
+  getConsumerGroupsListFetchingStatus,
+  (status) => status === FetchStatus.fetched,
+);
+
+export const getConsumerGroupsList = createSelector(consumerGroupsState, (consumerGroups) => consumerGroups);

+ 2 - 0
kafka-ui-react-app/src/redux/reducers/index.ts

@@ -2,6 +2,7 @@ import { combineReducers } from 'redux';
 import topics from './topics/reducer';
 import clusters from './clusters/reducer';
 import brokers from './brokers/reducer';
+import consumerGroups from './consumerGroups/reducer';
 import loader from './loader/reducer';
 import { RootState } from 'redux/interfaces';
 
@@ -9,5 +10,6 @@ export default combineReducers<RootState>({
   topics,
   clusters,
   brokers,
+  consumerGroups,
   loader,
 });