ソースを参照

Added consumer group details view.

Sofia Shnaidman 5 年 前
コミット
51dbfe40ca

+ 4 - 1
kafka-ui-react-app/mock/index.js

@@ -6,6 +6,7 @@ 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 consumerGroupDetails = require('./payload/consumerGroupDetails.json');
 
 const db = {
     clusters,
@@ -14,7 +15,8 @@ const db = {
     topics: topics.map((topic) => ({...topic, id: topic.name})),
     topicDetails,
     topicConfigs,
-    consumerGroups: consumerGroups.map((group) => ({...group, id: group.consumerGroupId}))
+    consumerGroups: consumerGroups.map((group) => ({...group, id: group.consumerGroupId})),
+    consumerGroupDetails
 };
 const server = jsonServer.create();
 const router = jsonServer.router(db);
@@ -34,6 +36,7 @@ server.use(
     '/clusters/:clusterName/metrics/broker': '/brokerMetrics/:clusterName',
     '/clusters/:clusterName/topics/:id': '/topicDetails',
     '/clusters/:clusterName/topics/:id/config': '/topicConfigs',
+    '/clusters/:clusterName/consumer-groups/:id': '/consumerGroupDetails',
   })
 );
 

+ 29 - 0
kafka-ui-react-app/mock/payload/consumerGroupDetails.json

@@ -0,0 +1,29 @@
+{
+    "consumerGroupId": "_fake.cluster.consumer_1",
+    "consumers": [
+        {
+            "consumerId": "_fake.cluster.consumer_1-1-1",
+            "topic": "my-topic",
+            "partition": 0,
+            "messagesBehind": 1246,
+            "currentOffset": 2834,
+            "endOffset": 2835
+        },
+        {
+            "consumerId": "_fake.cluster.consumer_2-2-2",
+            "topic": "docker-connect-status",
+            "partition": 1,
+            "messagesBehind": 678,
+            "currentOffset": 234,
+            "endOffset": 246
+        },
+        {
+            "consumerId": "_fake.cluster.consumer_2-2-2",
+            "topic": "docker-connect-status",
+            "partition": 2,
+            "messagesBehind": 143,
+            "currentOffset": 123,
+            "endOffset": 134
+        }
+    ]
+}

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

@@ -6,6 +6,7 @@ import {
 } from 'react-router-dom';
 import ListContainer from './List/ListContainer';
 import PageLoader from 'components/common/PageLoader/PageLoader';
+import DetailsContainer from './Details/DetailsContainer';
 
 interface Props {
   clusterName: ClusterName;
@@ -24,6 +25,7 @@ const ConsumerGroups: React.FC<Props> = ({
     return (
       <Switch>
         <Route exact path="/clusters/:clusterName/consumer-groups" component={ListContainer} />
+        <Route path="/clusters/:clusterName/consumer-groups/:consumerGroupID" component={DetailsContainer} />
       </Switch>
     );
   }

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

@@ -0,0 +1,68 @@
+import React from 'react';
+import { ClusterName } from 'redux/interfaces';
+import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
+import { clusterConsumerGroupsPath } from 'lib/paths';
+import { ConsumerGroupID, ConsumerGroup, ConsumerGroupDetails } from 'redux/interfaces/consumerGroup';
+import { Consumer } from '../../../redux/interfaces/consumerGroup';
+import ListItem from './ListItem';
+
+interface Props extends ConsumerGroup, ConsumerGroupDetails {
+  clusterName: ClusterName;
+  consumerGroupID: ConsumerGroupID;
+  consumers: (Consumer)[];
+  fetchConsumerGroupDetails: (clusterName: ClusterName, consumerGroupID: ConsumerGroupID) => void;
+}
+
+const Details: React.FC<Props> = ({
+  clusterName,
+  consumerGroupID,
+  consumers,
+  fetchConsumerGroupDetails
+}) => {
+
+  React.useEffect(
+    () => { fetchConsumerGroupDetails(clusterName, consumerGroupID); },
+    [fetchConsumerGroupDetails, clusterName, consumerGroupID],
+  );
+  const items = consumers || [];
+
+  return (
+    <div className="section">
+      <div className="level">
+        <div className="level-item level-left">
+          <Breadcrumb links={[
+            { href: clusterConsumerGroupsPath(clusterName), label: 'All Consumer Groups' },
+          ]}>
+            {consumerGroupID}
+          </Breadcrumb>
+        </div>
+      </div>
+
+      <div className="box">
+      <table className="table is-striped is-fullwidth">
+          <thead>
+            <tr>
+              <th>Consumer ID</th>
+              <th>Topic</th>
+              <th>Partition</th>
+              <th>Messages behind</th>
+              <th>Current offset</th>
+              <th>End offset</th>
+            </tr>
+          </thead>
+          <tbody>
+            {items
+              .map((consumer, index) => (
+                <ListItem
+                  key={`consumers-list-item-key-${index}`}
+                  {...{clusterName, ...consumer}}
+                />
+              ))}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  );
+};
+
+export default Details;

+ 29 - 0
kafka-ui-react-app/src/components/ConsumerGroups/Details/DetailsContainer.ts

@@ -0,0 +1,29 @@
+import { connect } from 'react-redux';
+import Details from './Details';
+import {ClusterName, RootState} from 'redux/interfaces';
+import { withRouter, RouteComponentProps } from 'react-router-dom';
+import { getIsConsumerGroupDetailsFetched, getConsumerGroupByID } from 'redux/reducers/consumerGroups/selectors';
+import { ConsumerGroupID } from 'redux/interfaces/consumerGroup';
+import { fetchConsumerGroupDetails } from 'redux/actions/thunks';
+
+interface RouteProps {
+  clusterName: ClusterName;
+  consumerGroupID: string;
+}
+
+interface OwnProps extends RouteComponentProps<RouteProps> { }
+
+const mapStateToProps = (state: RootState, { match: { params: { consumerGroupID, clusterName } } }: OwnProps) => ({
+  clusterName,
+  consumerGroupID,
+  isFetched: getIsConsumerGroupDetailsFetched(state),
+  ...getConsumerGroupByID(state, consumerGroupID)
+});
+
+const mapDispatchToProps = {
+  fetchConsumerGroupDetails: (clusterName: ClusterName, consumerGroupID: ConsumerGroupID) => fetchConsumerGroupDetails(clusterName, consumerGroupID),
+};
+
+export default withRouter(
+  connect(mapStateToProps, mapDispatchToProps)(Details)
+);

+ 38 - 0
kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.tsx

@@ -0,0 +1,38 @@
+import React from 'react';
+import { Consumer } from 'redux/interfaces/consumerGroup';
+import { NavLink } from 'react-router-dom';
+import { ClusterName } from '../../../redux/interfaces/cluster';
+
+
+interface Props extends Consumer {
+  clusterName: ClusterName;
+}
+
+const ListItem: React.FC<Props> = ({
+  clusterName,
+  consumerId,
+  topic,
+  partition,
+  messagesBehind,
+  currentOffset,
+  endOffset
+}) => {
+  return (
+    <tr>
+      <td>
+        {consumerId}
+      </td>
+      <td>
+        <NavLink exact to={`/clusters/${clusterName}/topics/${topic}`} activeClassName="is-active" className="title is-6">
+          {topic}
+        </NavLink>
+      </td>
+      <td>{partition}</td>
+      <td>{messagesBehind}</td>
+      <td>{currentOffset}</td>
+      <td>{endOffset}</td>
+    </tr>
+  );
+}
+
+export default ListItem;

+ 2 - 3
kafka-ui-react-app/src/components/ConsumerGroups/List/ListItem.tsx

@@ -9,12 +9,11 @@ const ListItem: React.FC<ConsumerGroup> = ({
 }) => {
   return (
     <tr>
-      {/* <td>
+      <td>
         <NavLink exact to={`consumer-groups/${consumerGroupId}`} activeClassName="is-active" className="title is-6">
           {consumerGroupId}
         </NavLink>
-      </td> */}
-      <td>{consumerGroupId}</td>
+      </td>
       <td>{numConsumers}</td>
       <td>{numTopics}</td>
     </tr>

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

@@ -30,4 +30,8 @@ export enum ActionType {
   GET_CONSUMER_GROUPS__REQUEST = 'GET_CONSUMER_GROUPS__REQUEST',
   GET_CONSUMER_GROUPS__SUCCESS = 'GET_CONSUMER_GROUPS__SUCCESS',
   GET_CONSUMER_GROUPS__FAILURE = 'GET_CONSUMER_GROUPS__FAILURE',
+
+  GET_CONSUMER_GROUP_DETAILS__REQUEST = 'GET_CONSUMER_GROUP_DETAILS__REQUEST',
+  GET_CONSUMER_GROUP_DETAILS__SUCCESS = 'GET_CONSUMER_GROUP_DETAILS__SUCCESS',
+  GET_CONSUMER_GROUP_DETAILS__FAILURE = 'GET_CONSUMER_GROUP_DETAILS__FAILURE',
 };

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

@@ -1,6 +1,6 @@
 import { createAsyncAction} from 'typesafe-actions';
 import { ActionType } from 'redux/actionType';
-import { ConsumerGroup } from '../interfaces/consumerGroup';
+import { ConsumerGroup, ConsumerGroupID, ConsumerGroupDetails } from '../interfaces/consumerGroup';
 import {
   Broker,
   BrokerMetrics,
@@ -58,3 +58,9 @@ export const fetchConsumerGroupsAction = createAsyncAction(
   ActionType.GET_CONSUMER_GROUPS__SUCCESS,
   ActionType.GET_CONSUMER_GROUPS__FAILURE,
 )<undefined, ConsumerGroup[], undefined>();
+
+export const fetchConsumerGroupDetailsAction = createAsyncAction(
+  ActionType.GET_CONSUMER_GROUP_DETAILS__REQUEST,
+  ActionType.GET_CONSUMER_GROUP_DETAILS__SUCCESS,
+  ActionType.GET_CONSUMER_GROUP_DETAILS__FAILURE,
+)<undefined, { consumerGroupID: ConsumerGroupID, details: ConsumerGroupDetails }, undefined>();

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

@@ -1,5 +1,6 @@
 import * as api from 'redux/api';
 import * as actions from './actions';
+import { ConsumerGroupID } from '../interfaces/consumerGroup';
 import {
   PromiseThunk,
   Cluster,
@@ -87,3 +88,13 @@ export const fetchConsumerGroupsList = (clusterName: ClusterName): PromiseThunk<
     dispatch(actions.fetchConsumerGroupsAction.failure());
   }
 };
+
+export const fetchConsumerGroupDetails = (clusterName: ClusterName, consumerGroupID: ConsumerGroupID): PromiseThunk<void> => async (dispatch) => {
+  dispatch(actions.fetchConsumerGroupDetailsAction.request());
+  try {
+    const consumerGroupDetails = await api.getConsumerGroupDetails(clusterName, consumerGroupID);
+    dispatch(actions.fetchConsumerGroupDetailsAction.success({ consumerGroupID, details: consumerGroupDetails }));
+  } catch (e) {
+    dispatch(actions.fetchConsumerGroupDetailsAction.failure());
+  }
+};

+ 5 - 1
kafka-ui-react-app/src/redux/api/consumerGroups.ts

@@ -1,8 +1,12 @@
 import { ClusterName } from '../interfaces/cluster';
-import { ConsumerGroup } from '../interfaces/consumerGroup';
+import { ConsumerGroup, ConsumerGroupID, ConsumerGroupDetails } 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());
+
+export const getConsumerGroupDetails = (clusterName: ClusterName, consumerGroupID: ConsumerGroupID): Promise<ConsumerGroupDetails> =>
+  fetch(`${BASE_URL}/clusters/${clusterName}/consumer-groups/${consumerGroupID}`, { ...BASE_PARAMS })
+    .then(res => res.json());

+ 27 - 1
kafka-ui-react-app/src/redux/interfaces/consumerGroup.ts

@@ -1,5 +1,31 @@
+export type ConsumerGroupID = string;
+
 export interface ConsumerGroup {
-  consumerGroupId: string;
+  consumerGroupId: ConsumerGroupID;
   numConsumers: number;
   numTopics: number;
+}
+
+export interface ConsumerGroupDetails {
+  consumerGroupId: ConsumerGroupID;
+  numConsumers: number;
+  numTopics: number;
+  consumers: Consumer[];
+}
+
+export interface Consumer {
+  consumerId: string;
+  topic: string;
+  partition: number;
+  messagesBehind: number;
+  currentOffset: number;
+  endOffset: number;
+}
+
+export interface ConsumerGroupDetailedInfo extends ConsumerGroup, ConsumerGroupDetails {
+}
+
+export interface ConsumerGroupsState {
+  byID: { [consumerGroupID: string]: ConsumerGroupDetailedInfo },
+  allIDs: string[]
 }

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

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

+ 39 - 3
kafka-ui-react-app/src/redux/reducers/consumerGroups/reducer.ts

@@ -1,12 +1,48 @@
 import { Action, ConsumerGroup } from 'redux/interfaces';
 import { ActionType } from 'redux/actionType';
+import { ConsumerGroupsState } from '../../interfaces/consumerGroup';
 
-export const initialState: ConsumerGroup[] = [];
+export const initialState: ConsumerGroupsState = {
+  byID: {},
+  allIDs: []
+};
+
+const updateConsumerGroupsList = (state: ConsumerGroupsState, payload: ConsumerGroup[]): ConsumerGroupsState => {
+  const initialMemo: ConsumerGroupsState = {
+    ...state,
+    allIDs: []
+  };
+
+  return payload.reduce(
+    (memo: ConsumerGroupsState, consumerGroup) => {
+      const {consumerGroupId} = consumerGroup;
+      memo.byID[consumerGroupId] = {
+        ...memo.byID[consumerGroupId],
+        ...consumerGroup,
+      };
+      memo.allIDs.push(consumerGroupId);
+
+      return memo;
+    },
+    initialMemo,
+  );
+};
 
-const reducer = (state = initialState, action: Action): ConsumerGroup[] => {
+const reducer = (state = initialState, action: Action): ConsumerGroupsState => {
   switch (action.type) {
     case ActionType.GET_CONSUMER_GROUPS__SUCCESS:
-      return action.payload;
+      return updateConsumerGroupsList(state, action.payload);
+    case ActionType.GET_CONSUMER_GROUP_DETAILS__SUCCESS:
+      return {
+        ...state,
+        byID: {
+          ...state.byID,
+          [action.payload.consumerGroupID]: {
+            ...state.byID[action.payload.consumerGroupID],
+            ...action.payload.details,
+          }
+        }
+      };
     default:
       return state;
   }

+ 29 - 3
kafka-ui-react-app/src/redux/reducers/consumerGroups/selectors.ts

@@ -1,15 +1,41 @@
 import { createSelector } from 'reselect';
-import { ConsumerGroup, RootState, FetchStatus } from 'redux/interfaces';
+import { RootState, FetchStatus } from 'redux/interfaces';
 import { createFetchingSelector } from 'redux/reducers/loader/selectors';
+import { ConsumerGroupID, ConsumerGroupsState } from '../../interfaces/consumerGroup';
 
 
-const consumerGroupsState = ({ consumerGroups }: RootState): ConsumerGroup[] => consumerGroups;
+const consumerGroupsState = ({ consumerGroups }: RootState): ConsumerGroupsState => consumerGroups;
+
+const getConsumerGroupsMap = (state: RootState) => consumerGroupsState(state).byID;
 
 const getConsumerGroupsListFetchingStatus = createFetchingSelector('GET_CONSUMER_GROUPS');
+const getConsumerGroupDetailsFetchingStatus = createFetchingSelector('GET_CONSUMER_GROUP_DETAILS');
 
 export const getIsConsumerGroupsListFetched = createSelector(
   getConsumerGroupsListFetchingStatus,
   (status) => status === FetchStatus.fetched,
 );
 
-export const getConsumerGroupsList = createSelector(consumerGroupsState, (consumerGroups) => consumerGroups);
+export const getIsConsumerGroupDetailsFetched = createSelector(
+  getConsumerGroupDetailsFetchingStatus,
+  (status) => status === FetchStatus.fetched,
+);
+
+export const getConsumerGroupsList = createSelector(
+  getIsConsumerGroupsListFetched,
+  getConsumerGroupsMap,
+  (isFetched, byID) => {
+    if (!isFetched) {
+      return [];
+    }
+    return Object.keys(byID).map( (key) => byID[key]);
+  },
+);
+
+const getConsumerGroupID = (_: RootState, consumerGroupID: ConsumerGroupID) => consumerGroupID;
+
+export const getConsumerGroupByID = createSelector(
+  getConsumerGroupsMap,
+  getConsumerGroupID,
+  (consumerGroups, consumerGroupID) => consumerGroups[consumerGroupID],
+);