WIP: [issues#121] Topic Details: Display consumers (#448)
* [issues#121] Topic Details: Display consumers * [issues#121] Topic Details: Display consumers * [issues#121] Topic Details: Display consumers * [issues#121] Topic Details: Display consumers * [issues#121] Topic Details: Display consumers * [issues#121] Topic Details: Display consumers Co-authored-by: mbovtryuk <mbovtryuk@provectus.com>
This commit is contained in:
parent
69c034c793
commit
5d65967bc6
14 changed files with 279 additions and 0 deletions
|
@ -0,0 +1,53 @@
|
|||
import React from 'react';
|
||||
import { Topic, TopicDetails, ConsumerGroup } from 'generated-sources';
|
||||
import { ClusterName, TopicName } from 'redux/interfaces';
|
||||
import ListItem from 'components/ConsumerGroups/List/ListItem';
|
||||
|
||||
interface Props extends Topic, TopicDetails {
|
||||
clusterName: ClusterName;
|
||||
topicName: TopicName;
|
||||
consumerGroups: Array<ConsumerGroup>;
|
||||
fetchTopicConsumerGroups(
|
||||
clusterName: ClusterName,
|
||||
topicName: TopicName
|
||||
): void;
|
||||
}
|
||||
|
||||
const TopicConsumerGroups: React.FC<Props> = ({
|
||||
consumerGroups,
|
||||
fetchTopicConsumerGroups,
|
||||
clusterName,
|
||||
topicName,
|
||||
}) => {
|
||||
React.useEffect(() => {
|
||||
fetchTopicConsumerGroups(clusterName, topicName);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="box">
|
||||
{consumerGroups.length > 0 ? (
|
||||
<table className="table is-striped is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Consumer group ID</th>
|
||||
<th>Num of consumers</th>
|
||||
<th>Num of topics</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{consumerGroups.map((consumerGroup) => (
|
||||
<ListItem
|
||||
key={consumerGroup.consumerGroupId}
|
||||
consumerGroup={consumerGroup}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
'No active consumer groups'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopicConsumerGroups;
|
|
@ -0,0 +1,34 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { RootState, TopicName, ClusterName } from 'redux/interfaces';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { fetchTopicConsumerGroups } from 'redux/actions';
|
||||
import TopicConsumerGroups from 'components/Topics/Topic/Details/ConsumerGroups/ConsumerGroups';
|
||||
import { getTopicConsumerGroups } from 'redux/reducers/topics/selectors';
|
||||
|
||||
interface RouteProps {
|
||||
clusterName: ClusterName;
|
||||
topicName: TopicName;
|
||||
}
|
||||
|
||||
type OwnProps = RouteComponentProps<RouteProps>;
|
||||
|
||||
const mapStateToProps = (
|
||||
state: RootState,
|
||||
{
|
||||
match: {
|
||||
params: { topicName, clusterName },
|
||||
},
|
||||
}: OwnProps
|
||||
) => ({
|
||||
consumerGroups: getTopicConsumerGroups(state),
|
||||
topicName,
|
||||
clusterName,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchTopicConsumerGroups,
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
connect(mapStateToProps, mapDispatchToProps)(TopicConsumerGroups)
|
||||
);
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import ConsumerGroups from 'components/Topics/Topic/Details/ConsumerGroups/ConsumerGroups';
|
||||
|
||||
describe('Details', () => {
|
||||
const mockFn = jest.fn();
|
||||
const mockClusterName = 'local';
|
||||
const mockTopicName = 'local';
|
||||
const mockWithConsumerGroup = [
|
||||
{
|
||||
clusterId: '1',
|
||||
consumerGroupId: '1',
|
||||
},
|
||||
];
|
||||
|
||||
it("don't render ConsumerGroups in Topic", () => {
|
||||
const component = shallow(
|
||||
<ConsumerGroups
|
||||
clusterName={mockClusterName}
|
||||
consumerGroups={[]}
|
||||
name={mockTopicName}
|
||||
fetchTopicConsumerGroups={mockFn}
|
||||
topicName={mockTopicName}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.exists('.table')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('render ConsumerGroups in Topic', () => {
|
||||
const component = shallow(
|
||||
<ConsumerGroups
|
||||
clusterName={mockClusterName}
|
||||
consumerGroups={mockWithConsumerGroup}
|
||||
name={mockTopicName}
|
||||
fetchTopicConsumerGroups={mockFn}
|
||||
topicName={mockTopicName}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.exists('.table')).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -7,12 +7,14 @@ import {
|
|||
clusterTopicPath,
|
||||
clusterTopicMessagesPath,
|
||||
clusterTopicsPath,
|
||||
clusterTopicConsumerGroupsPath,
|
||||
clusterTopicEditPath,
|
||||
} from 'lib/paths';
|
||||
import ClusterContext from 'components/contexts/ClusterContext';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
|
||||
import OverviewContainer from './Overview/OverviewContainer';
|
||||
import TopicConsumerGroupsContainer from './ConsumerGroups/ConsumerGroupsContainer';
|
||||
import MessagesContainer from './Messages/MessagesContainer';
|
||||
import SettingsContainer from './Settings/SettingsContainer';
|
||||
|
||||
|
@ -64,6 +66,14 @@ const Details: React.FC<Props> = ({
|
|||
>
|
||||
Messages
|
||||
</NavLink>
|
||||
<NavLink
|
||||
exact
|
||||
to={clusterTopicConsumerGroupsPath(clusterName, topicName)}
|
||||
className="navbar-item is-tab"
|
||||
activeClassName="is-active"
|
||||
>
|
||||
Consumers
|
||||
</NavLink>
|
||||
<NavLink
|
||||
exact
|
||||
to={clusterTopicSettingsPath(clusterName, topicName)}
|
||||
|
@ -128,6 +138,11 @@ const Details: React.FC<Props> = ({
|
|||
path="/ui/clusters/:clusterName/topics/:topicName"
|
||||
component={OverviewContainer}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/ui/clusters/:clusterName/topics/:topicName/consumergroups"
|
||||
component={TopicConsumerGroupsContainer}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -56,6 +56,10 @@ export const clusterTopicEditPath = (
|
|||
clusterName: ClusterName,
|
||||
topicName: TopicName
|
||||
) => `${clusterTopicsPath(clusterName)}/${topicName}/edit`;
|
||||
export const clusterTopicConsumerGroupsPath = (
|
||||
clusterName: ClusterName,
|
||||
topicName: TopicName
|
||||
) => `${clusterTopicsPath(clusterName)}/${topicName}/consumergroups`;
|
||||
|
||||
// Kafka Connect
|
||||
export const clusterConnectsPath = (clusterName: ClusterName) =>
|
||||
|
|
|
@ -5,6 +5,8 @@ import {
|
|||
import * as actions from 'redux/actions';
|
||||
import { TopicColumnsToSort } from 'generated-sources';
|
||||
|
||||
import { mockTopicsState } from './fixtures';
|
||||
|
||||
describe('Actions', () => {
|
||||
describe('fetchClusterStatsAction', () => {
|
||||
it('creates a REQUEST action', () => {
|
||||
|
@ -133,6 +135,29 @@ describe('Actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('fetchTopicConsumerGroups', () => {
|
||||
it('creates a REQUEST action', () => {
|
||||
expect(actions.fetchTopicConsumerGroupsAction.request()).toEqual({
|
||||
type: 'GET_TOPIC_CONSUMER_GROUPS__REQUEST',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a SUCCESS action', () => {
|
||||
expect(
|
||||
actions.fetchTopicConsumerGroupsAction.success(mockTopicsState)
|
||||
).toEqual({
|
||||
type: 'GET_TOPIC_CONSUMER_GROUPS__SUCCESS',
|
||||
payload: mockTopicsState,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a FAILURE action', () => {
|
||||
expect(actions.fetchTopicConsumerGroupsAction.failure()).toEqual({
|
||||
type: 'GET_TOPIC_CONSUMER_GROUPS__FAILURE',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setTopicsSearchAction', () => {
|
||||
it('creartes SET_TOPICS_SEARCH', () => {
|
||||
expect(actions.setTopicsSearchAction('test')).toEqual({
|
||||
|
|
|
@ -34,3 +34,13 @@ export const schema: SchemaSubject = {
|
|||
id: 1,
|
||||
compatibilityLevel: CompatibilityLevelCompatibilityEnum.BACKWARD,
|
||||
};
|
||||
|
||||
export const mockTopicsState = {
|
||||
byName: {},
|
||||
allNames: [],
|
||||
totalPages: 1,
|
||||
messages: [],
|
||||
search: '',
|
||||
orderBy: null,
|
||||
consumerGroups: [],
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@ import fetchMock from 'fetch-mock-jest';
|
|||
import * as actions from 'redux/actions/actions';
|
||||
import * as thunks from 'redux/actions/thunks';
|
||||
import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
|
||||
import { mockTopicsState } from 'redux/actions/__test__/fixtures';
|
||||
|
||||
const store = mockStoreCreator;
|
||||
|
||||
|
@ -93,4 +94,42 @@ describe('Thunks', () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchTopicConsumerGroups', () => {
|
||||
it('GET_TOPIC_CONSUMER_GROUPS__FAILURE', async () => {
|
||||
fetchMock.getOnce(
|
||||
`api/clusters/${clusterName}/topics/${topicName}/consumergroups`,
|
||||
404
|
||||
);
|
||||
try {
|
||||
await store.dispatch(
|
||||
thunks.fetchTopicConsumerGroups(clusterName, topicName)
|
||||
);
|
||||
} catch (error) {
|
||||
expect(error.status).toEqual(404);
|
||||
expect(store.getActions()).toEqual([
|
||||
actions.fetchTopicConsumerGroupsAction.request(),
|
||||
actions.fetchTopicConsumerGroupsAction.failure(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('GET_TOPIC_CONSUMER_GROUPS__SUCCESS', async () => {
|
||||
fetchMock.getOnce(
|
||||
`api/clusters/${clusterName}/topics/${topicName}/consumergroups`,
|
||||
200
|
||||
);
|
||||
try {
|
||||
await store.dispatch(
|
||||
thunks.fetchTopicConsumerGroups(clusterName, topicName)
|
||||
);
|
||||
} catch (error) {
|
||||
expect(error.status).toEqual(200);
|
||||
expect(store.getActions()).toEqual([
|
||||
actions.fetchTopicConsumerGroupsAction.request(),
|
||||
actions.fetchTopicConsumerGroupsAction.success(mockTopicsState),
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -241,3 +241,9 @@ export const setTopicsSearchAction =
|
|||
export const setTopicsOrderByAction = createAction(
|
||||
'SET_TOPICS_ORDER_BY'
|
||||
)<TopicColumnsToSort>();
|
||||
|
||||
export const fetchTopicConsumerGroupsAction = createAsyncAction(
|
||||
'GET_TOPIC_CONSUMER_GROUPS__REQUEST',
|
||||
'GET_TOPIC_CONSUMER_GROUPS__SUCCESS',
|
||||
'GET_TOPIC_CONSUMER_GROUPS__FAILURE'
|
||||
)<undefined, TopicsState, undefined>();
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
TopicUpdate,
|
||||
TopicConfig,
|
||||
TopicColumnsToSort,
|
||||
ConsumerGroupsApi,
|
||||
} from 'generated-sources';
|
||||
import {
|
||||
PromiseThunkResult,
|
||||
|
@ -26,6 +27,9 @@ import { getResponse } from 'lib/errorHandling';
|
|||
const apiClientConf = new Configuration(BASE_PARAMS);
|
||||
export const topicsApiClient = new TopicsApi(apiClientConf);
|
||||
export const messagesApiClient = new MessagesApi(apiClientConf);
|
||||
export const topicConsumerGroupsApiClient = new ConsumerGroupsApi(
|
||||
apiClientConf
|
||||
);
|
||||
|
||||
export interface FetchTopicsListParams {
|
||||
clusterName: ClusterName;
|
||||
|
@ -316,3 +320,32 @@ export const deleteTopic =
|
|||
dispatch(actions.deleteTopicAction.failure());
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchTopicConsumerGroups =
|
||||
(clusterName: ClusterName, topicName: TopicName): PromiseThunkResult =>
|
||||
async (dispatch, getState) => {
|
||||
dispatch(actions.fetchTopicConsumerGroupsAction.request());
|
||||
try {
|
||||
const consumerGroups =
|
||||
await topicConsumerGroupsApiClient.getTopicConsumerGroups({
|
||||
clusterName,
|
||||
topicName,
|
||||
});
|
||||
const state = getState().topics;
|
||||
const newState = {
|
||||
...state,
|
||||
byName: {
|
||||
...state.byName,
|
||||
[topicName]: {
|
||||
...state.byName[topicName],
|
||||
consumerGroups: {
|
||||
...consumerGroups,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
dispatch(actions.fetchTopicConsumerGroupsAction.success(newState));
|
||||
} catch (e) {
|
||||
dispatch(actions.fetchTopicConsumerGroupsAction.failure());
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
TopicConfig,
|
||||
TopicCreation,
|
||||
GetTopicMessagesRequest,
|
||||
ConsumerGroup,
|
||||
TopicColumnsToSort,
|
||||
} from 'generated-sources';
|
||||
|
||||
|
@ -48,6 +49,7 @@ export interface TopicsState {
|
|||
messages: TopicMessage[];
|
||||
search: string;
|
||||
orderBy: TopicColumnsToSort | null;
|
||||
consumerGroups: ConsumerGroup[];
|
||||
}
|
||||
|
||||
export type TopicFormFormattedParams = TopicCreation['configs'];
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
clearMessagesTopicAction,
|
||||
setTopicsSearchAction,
|
||||
setTopicsOrderByAction,
|
||||
fetchTopicConsumerGroupsAction,
|
||||
} from 'redux/actions';
|
||||
import reducer from 'redux/reducers/topics/reducer';
|
||||
|
||||
|
@ -21,6 +22,7 @@ const state = {
|
|||
totalPages: 1,
|
||||
search: '',
|
||||
orderBy: null,
|
||||
consumerGroups: [],
|
||||
};
|
||||
|
||||
describe('topics reducer', () => {
|
||||
|
@ -30,6 +32,7 @@ describe('topics reducer', () => {
|
|||
...state,
|
||||
byName: {},
|
||||
allNames: [],
|
||||
consumerGroups: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -59,4 +62,12 @@ describe('topics reducer', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('topic consumer groups', () => {
|
||||
it('GET_TOPIC_CONSUMER_GROUPS__SUCCESS', () => {
|
||||
expect(
|
||||
reducer(state, fetchTopicConsumerGroupsAction.success(state))
|
||||
).toEqual(state);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ export const initialState: TopicsState = {
|
|||
messages: [],
|
||||
search: '',
|
||||
orderBy: null,
|
||||
consumerGroups: [],
|
||||
};
|
||||
|
||||
const transformTopicMessages = (
|
||||
|
@ -43,6 +44,7 @@ const reducer = (state = initialState, action: Action): TopicsState => {
|
|||
case getType(actions.fetchTopicDetailsAction.success):
|
||||
case getType(actions.fetchTopicConfigAction.success):
|
||||
case getType(actions.createTopicAction.success):
|
||||
case getType(actions.fetchTopicConsumerGroupsAction.success):
|
||||
case getType(actions.updateTopicAction.success):
|
||||
return action.payload;
|
||||
case getType(actions.fetchTopicMessagesAction.success):
|
||||
|
|
|
@ -16,6 +16,8 @@ export const getTopicMessages = (state: RootState) =>
|
|||
topicsState(state).messages;
|
||||
export const getTopicListTotalPages = (state: RootState) =>
|
||||
topicsState(state).totalPages;
|
||||
export const getTopicConsumerGroups = (state: RootState) =>
|
||||
topicsState(state).consumerGroups;
|
||||
|
||||
const getTopicListFetchingStatus = createFetchingSelector('GET_TOPICS');
|
||||
const getTopicDetailsFetchingStatus =
|
||||
|
|
Loading…
Add table
Reference in a new issue