Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: [issues#121] Topic Details: Display consumers #448

Merged
merged 8 commits into from
May 14, 2021
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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)
);
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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)}
Expand Down Expand Up @@ -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>
);
Expand Down
4 changes: 4 additions & 0 deletions kafka-ui-react-app/src/lib/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
25 changes: 25 additions & 0 deletions kafka-ui-react-app/src/redux/actions/__test__/actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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({
Expand Down
10 changes: 10 additions & 0 deletions kafka-ui-react-app/src/redux/actions/__test__/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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),
]);
}
});
});
});
6 changes: 6 additions & 0 deletions kafka-ui-react-app/src/redux/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
33 changes: 33 additions & 0 deletions kafka-ui-react-app/src/redux/actions/thunks/topics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
TopicUpdate,
TopicConfig,
TopicColumnsToSort,
ConsumerGroupsApi,
} from 'generated-sources';
import {
PromiseThunkResult,
Expand All @@ -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;
Expand Down Expand Up @@ -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());
}
};
2 changes: 2 additions & 0 deletions kafka-ui-react-app/src/redux/interfaces/topic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
TopicConfig,
TopicCreation,
GetTopicMessagesRequest,
ConsumerGroup,
TopicColumnsToSort,
} from 'generated-sources';

Expand Down Expand Up @@ -48,6 +49,7 @@ export interface TopicsState {
messages: TopicMessage[];
search: string;
orderBy: TopicColumnsToSort | null;
consumerGroups: ConsumerGroup[];
}

export type TopicFormFormattedParams = TopicCreation['configs'];
Expand Down
Loading