Implement consumer group deleting (#600)
* Implement consumer group deleting
This commit is contained in:
parent
7482465943
commit
64a5985e81
11 changed files with 460 additions and 7 deletions
|
@ -9,31 +9,50 @@ import {
|
|||
ConsumerTopicPartitionDetail,
|
||||
} from 'generated-sources';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
import ListItem from './ListItem';
|
||||
|
||||
interface Props extends ConsumerGroup, ConsumerGroupDetails {
|
||||
export interface Props extends ConsumerGroup, ConsumerGroupDetails {
|
||||
clusterName: ClusterName;
|
||||
consumerGroupID: ConsumerGroupID;
|
||||
consumerGroupId: ConsumerGroupID;
|
||||
consumers?: ConsumerTopicPartitionDetail[];
|
||||
isFetched: boolean;
|
||||
isDeleted: boolean;
|
||||
fetchConsumerGroupDetails: (
|
||||
clusterName: ClusterName,
|
||||
consumerGroupID: ConsumerGroupID
|
||||
) => void;
|
||||
deleteConsumerGroup: (clusterName: string, id: ConsumerGroupID) => void;
|
||||
}
|
||||
|
||||
const Details: React.FC<Props> = ({
|
||||
clusterName,
|
||||
consumerGroupID,
|
||||
consumerGroupId,
|
||||
consumers,
|
||||
isFetched,
|
||||
isDeleted,
|
||||
fetchConsumerGroupDetails,
|
||||
deleteConsumerGroup,
|
||||
}) => {
|
||||
React.useEffect(() => {
|
||||
fetchConsumerGroupDetails(clusterName, consumerGroupID);
|
||||
}, [fetchConsumerGroupDetails, clusterName, consumerGroupID]);
|
||||
fetchConsumerGroupDetails(clusterName, consumerGroupId);
|
||||
}, [fetchConsumerGroupDetails, clusterName, consumerGroupId]);
|
||||
const items = consumers || [];
|
||||
const [isConfirmationModelVisible, setIsConfirmationModelVisible] =
|
||||
React.useState<boolean>(false);
|
||||
const history = useHistory();
|
||||
|
||||
const onDelete = () => {
|
||||
setIsConfirmationModelVisible(false);
|
||||
deleteConsumerGroup(clusterName, consumerGroupId);
|
||||
};
|
||||
React.useEffect(() => {
|
||||
if (isDeleted) {
|
||||
history.push(clusterConsumerGroupsPath(clusterName));
|
||||
}
|
||||
}, [isDeleted]);
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
|
@ -47,13 +66,24 @@ const Details: React.FC<Props> = ({
|
|||
},
|
||||
]}
|
||||
>
|
||||
{consumerGroupID}
|
||||
{consumerGroupId}
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isFetched ? (
|
||||
<div className="box">
|
||||
<div className="level">
|
||||
<div className="level-item level-right buttons">
|
||||
<button
|
||||
type="button"
|
||||
className="button is-danger"
|
||||
onClick={() => setIsConfirmationModelVisible(true)}
|
||||
>
|
||||
Delete consumer group
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table className="table is-striped is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -80,6 +110,13 @@ const Details: React.FC<Props> = ({
|
|||
) : (
|
||||
<PageLoader />
|
||||
)}
|
||||
<ConfirmationModal
|
||||
isOpen={isConfirmationModelVisible}
|
||||
onCancel={() => setIsConfirmationModelVisible(false)}
|
||||
onConfirm={onDelete}
|
||||
>
|
||||
Are you sure you want to delete this consumer group?
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,10 +3,14 @@ import { ClusterName, RootState } from 'redux/interfaces';
|
|||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import {
|
||||
getIsConsumerGroupDetailsFetched,
|
||||
getIsConsumerGroupsDeleted,
|
||||
getConsumerGroupByID,
|
||||
} from 'redux/reducers/consumerGroups/selectors';
|
||||
import { ConsumerGroupID } from 'redux/interfaces/consumerGroup';
|
||||
import { fetchConsumerGroupDetails } from 'redux/actions/thunks';
|
||||
import {
|
||||
deleteConsumerGroup,
|
||||
fetchConsumerGroupDetails,
|
||||
} from 'redux/actions/thunks';
|
||||
|
||||
import Details from './Details';
|
||||
|
||||
|
@ -28,6 +32,7 @@ const mapStateToProps = (
|
|||
clusterName,
|
||||
consumerGroupID,
|
||||
isFetched: getIsConsumerGroupDetailsFetched(state),
|
||||
isDeleted: getIsConsumerGroupsDeleted(state),
|
||||
...getConsumerGroupByID(state, consumerGroupID),
|
||||
});
|
||||
|
||||
|
@ -36,6 +41,8 @@ const mapDispatchToProps = {
|
|||
clusterName: ClusterName,
|
||||
consumerGroupID: ConsumerGroupID
|
||||
) => fetchConsumerGroupDetails(clusterName, consumerGroupID),
|
||||
deleteConsumerGroup: (clusterName: string, id: ConsumerGroupID) =>
|
||||
deleteConsumerGroup(clusterName, id),
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
import Details, { Props } from 'components/ConsumerGroups/Details/Details';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { StaticRouter } from 'react-router';
|
||||
|
||||
const mockHistory = {
|
||||
push: jest.fn(),
|
||||
};
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useHistory: () => mockHistory,
|
||||
}));
|
||||
|
||||
describe('Details component', () => {
|
||||
const setupWrapper = (props?: Partial<Props>) => (
|
||||
<Details
|
||||
clusterName="local"
|
||||
clusterId="local"
|
||||
consumerGroupId="test"
|
||||
isFetched
|
||||
isDeleted={false}
|
||||
fetchConsumerGroupDetails={jest.fn()}
|
||||
deleteConsumerGroup={jest.fn()}
|
||||
consumers={[
|
||||
{
|
||||
groupId: 'messages-consumer',
|
||||
consumerId:
|
||||
'consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d0',
|
||||
topic: 'messages',
|
||||
host: '/172.31.9.153',
|
||||
partition: 6,
|
||||
currentOffset: 394,
|
||||
endOffset: 394,
|
||||
messagesBehind: 0,
|
||||
},
|
||||
{
|
||||
groupId: 'messages-consumer',
|
||||
consumerId:
|
||||
'consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d1',
|
||||
topic: 'messages',
|
||||
host: '/172.31.9.153',
|
||||
partition: 7,
|
||||
currentOffset: 384,
|
||||
endOffset: 384,
|
||||
messagesBehind: 0,
|
||||
},
|
||||
]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
describe('when consumer gruops are NOT fetched', () => {
|
||||
it('Matches the snapshot', () => {
|
||||
expect(shallow(setupWrapper({ isFetched: false }))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when consumer gruops are fetched', () => {
|
||||
it('Matches the snapshot', () => {
|
||||
expect(shallow(setupWrapper())).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('onDelete', () => {
|
||||
it('calls deleteConsumerGroup', () => {
|
||||
const deleteConsumerGroup = jest.fn();
|
||||
const component = mount(
|
||||
<StaticRouter>{setupWrapper({ deleteConsumerGroup })}</StaticRouter>
|
||||
);
|
||||
component.find('button').at(0).simulate('click');
|
||||
component.update();
|
||||
component
|
||||
.find('ConfirmationModal')
|
||||
.find('button')
|
||||
.at(1)
|
||||
.simulate('click');
|
||||
expect(deleteConsumerGroup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('on ConfirmationModal cancel', () => {
|
||||
it('does not call deleteConsumerGroup', () => {
|
||||
const deleteConsumerGroup = jest.fn();
|
||||
const component = mount(
|
||||
<StaticRouter>{setupWrapper({ deleteConsumerGroup })}</StaticRouter>
|
||||
);
|
||||
component.find('button').at(0).simulate('click');
|
||||
component.update();
|
||||
component
|
||||
.find('ConfirmationModal')
|
||||
.find('button')
|
||||
.at(0)
|
||||
.simulate('click');
|
||||
expect(deleteConsumerGroup).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('after deletion', () => {
|
||||
it('calls history.push', () => {
|
||||
mount(
|
||||
<StaticRouter>{setupWrapper({ isDeleted: true })}</StaticRouter>
|
||||
);
|
||||
expect(mockHistory.push).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,152 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Details component when consumer gruops are NOT fetched Matches the snapshot 1`] = `
|
||||
<div
|
||||
className="section"
|
||||
>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<div
|
||||
className="level-item level-left"
|
||||
>
|
||||
<Breadcrumb
|
||||
links={
|
||||
Array [
|
||||
Object {
|
||||
"href": "/ui/clusters/local/consumer-groups",
|
||||
"label": "All Consumer Groups",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
test
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
<PageLoader />
|
||||
<ConfirmationModal
|
||||
isOpen={false}
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
>
|
||||
Are you sure you want to delete this consumer group?
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Details component when consumer gruops are fetched Matches the snapshot 1`] = `
|
||||
<div
|
||||
className="section"
|
||||
>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<div
|
||||
className="level-item level-left"
|
||||
>
|
||||
<Breadcrumb
|
||||
links={
|
||||
Array [
|
||||
Object {
|
||||
"href": "/ui/clusters/local/consumer-groups",
|
||||
"label": "All Consumer Groups",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
test
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<div
|
||||
className="level-item level-right buttons"
|
||||
>
|
||||
<button
|
||||
className="button is-danger"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Delete consumer group
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table
|
||||
className="table is-striped is-fullwidth"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Consumer ID
|
||||
</th>
|
||||
<th>
|
||||
Host
|
||||
</th>
|
||||
<th>
|
||||
Topic
|
||||
</th>
|
||||
<th>
|
||||
Partition
|
||||
</th>
|
||||
<th>
|
||||
Messages behind
|
||||
</th>
|
||||
<th>
|
||||
Current offset
|
||||
</th>
|
||||
<th>
|
||||
End offset
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ListItem
|
||||
clusterName="local"
|
||||
consumer={
|
||||
Object {
|
||||
"consumerId": "consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d0",
|
||||
"currentOffset": 394,
|
||||
"endOffset": 394,
|
||||
"groupId": "messages-consumer",
|
||||
"host": "/172.31.9.153",
|
||||
"messagesBehind": 0,
|
||||
"partition": 6,
|
||||
"topic": "messages",
|
||||
}
|
||||
}
|
||||
key="consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d0"
|
||||
/>
|
||||
<ListItem
|
||||
clusterName="local"
|
||||
consumer={
|
||||
Object {
|
||||
"consumerId": "consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d1",
|
||||
"currentOffset": 384,
|
||||
"endOffset": 384,
|
||||
"groupId": "messages-consumer",
|
||||
"host": "/172.31.9.153",
|
||||
"messagesBehind": 0,
|
||||
"partition": 7,
|
||||
"topic": "messages",
|
||||
}
|
||||
}
|
||||
key="consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d1"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
isOpen={false}
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
>
|
||||
Are you sure you want to delete this consumer group?
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
`;
|
|
@ -4,6 +4,7 @@ import {
|
|||
} from 'redux/reducers/schemas/__test__/fixtures';
|
||||
import * as actions from 'redux/actions';
|
||||
import { TopicColumnsToSort } from 'generated-sources';
|
||||
import { FailurePayload } from 'redux/interfaces';
|
||||
|
||||
import { mockTopicsState } from './fixtures';
|
||||
|
||||
|
@ -175,4 +176,30 @@ describe('Actions', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleting consumer group', () => {
|
||||
it('creates DELETE_CONSUMER_GROUP__REQUEST', () => {
|
||||
expect(actions.deleteConsumerGroupAction.request()).toEqual({
|
||||
type: 'DELETE_CONSUMER_GROUP__REQUEST',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates DELETE_CONSUMER_GROUP__SUCCESS', () => {
|
||||
expect(actions.deleteConsumerGroupAction.success('test')).toEqual({
|
||||
type: 'DELETE_CONSUMER_GROUP__SUCCESS',
|
||||
payload: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates DELETE_CONSUMER_GROUP__FAILURE', () => {
|
||||
const alert: FailurePayload = {
|
||||
subject: ['consumer-group', 'test'].join('-'),
|
||||
title: `Consumer Gropup Test`,
|
||||
};
|
||||
expect(actions.deleteConsumerGroupAction.failure({ alert })).toEqual({
|
||||
type: 'DELETE_CONSUMER_GROUP__FAILURE',
|
||||
payload: { alert },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import fetchMock from 'fetch-mock-jest';
|
||||
import * as actions from 'redux/actions/actions';
|
||||
import * as thunks from 'redux/actions/thunks';
|
||||
import { FailurePayload } from 'redux/interfaces';
|
||||
import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
|
||||
|
||||
const store = mockStoreCreator;
|
||||
const clusterName = 'local';
|
||||
const id = 'test';
|
||||
|
||||
describe('Consumer Groups Thunks', () => {
|
||||
afterEach(() => {
|
||||
fetchMock.restore();
|
||||
store.clearActions();
|
||||
});
|
||||
|
||||
describe('deleting consumer groups', () => {
|
||||
it('calls DELETE_CONSUMER_GROUP__SUCCESS after successful delete', async () => {
|
||||
fetchMock.deleteOnce(
|
||||
`/api/clusters/${clusterName}/consumer-groups/${id}`,
|
||||
200
|
||||
);
|
||||
|
||||
await store.dispatch(thunks.deleteConsumerGroup(clusterName, id));
|
||||
expect(store.getActions()).toEqual([
|
||||
actions.deleteConsumerGroupAction.request(),
|
||||
actions.deleteConsumerGroupAction.success(id),
|
||||
]);
|
||||
});
|
||||
|
||||
it('calls DELETE_CONSUMER_GROUP__FAILURE after successful delete', async () => {
|
||||
fetchMock.deleteOnce(
|
||||
`/api/clusters/${clusterName}/consumer-groups/${id}`,
|
||||
500
|
||||
);
|
||||
|
||||
await store.dispatch(thunks.deleteConsumerGroup(clusterName, id));
|
||||
const alert: FailurePayload = {
|
||||
subject: ['consumer-group', id].join('-'),
|
||||
title: `Consumer Gropup ${id}`,
|
||||
response: {
|
||||
body: undefined,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
},
|
||||
};
|
||||
expect(store.getActions()).toEqual([
|
||||
actions.deleteConsumerGroupAction.request(),
|
||||
actions.deleteConsumerGroupAction.failure({ alert }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -119,6 +119,12 @@ export const fetchConsumerGroupDetailsAction = createAsyncAction(
|
|||
undefined
|
||||
>();
|
||||
|
||||
export const deleteConsumerGroupAction = createAsyncAction(
|
||||
'DELETE_CONSUMER_GROUP__REQUEST',
|
||||
'DELETE_CONSUMER_GROUP__SUCCESS',
|
||||
'DELETE_CONSUMER_GROUP__FAILURE'
|
||||
)<undefined, ConsumerGroupID, { alert?: FailurePayload }>();
|
||||
|
||||
export const fetchSchemasByClusterNameAction = createAsyncAction(
|
||||
'GET_CLUSTER_SCHEMAS__REQUEST',
|
||||
'GET_CLUSTER_SCHEMAS__SUCCESS',
|
||||
|
|
|
@ -3,8 +3,10 @@ import {
|
|||
ConsumerGroupID,
|
||||
PromiseThunkResult,
|
||||
ClusterName,
|
||||
FailurePayload,
|
||||
} from 'redux/interfaces';
|
||||
import { BASE_PARAMS } from 'lib/constants';
|
||||
import { getResponse } from 'lib/errorHandling';
|
||||
import * as actions from 'redux/actions/actions';
|
||||
|
||||
const apiClientConf = new Configuration(BASE_PARAMS);
|
||||
|
@ -47,3 +49,27 @@ export const fetchConsumerGroupDetails =
|
|||
dispatch(actions.fetchConsumerGroupDetailsAction.failure());
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteConsumerGroup =
|
||||
(
|
||||
clusterName: ClusterName,
|
||||
consumerGroupID: ConsumerGroupID
|
||||
): PromiseThunkResult =>
|
||||
async (dispatch) => {
|
||||
dispatch(actions.deleteConsumerGroupAction.request());
|
||||
try {
|
||||
await consumerGroupsApiClient.deleteConsumerGroup({
|
||||
clusterName,
|
||||
id: consumerGroupID,
|
||||
});
|
||||
dispatch(actions.deleteConsumerGroupAction.success(consumerGroupID));
|
||||
} catch (e) {
|
||||
const response = await getResponse(e);
|
||||
const alert: FailurePayload = {
|
||||
subject: ['consumer-group', consumerGroupID].join('-'),
|
||||
title: `Consumer Gropup ${consumerGroupID}`,
|
||||
response,
|
||||
};
|
||||
dispatch(actions.deleteConsumerGroupAction.failure({ alert }));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { ConsumerGroupsState } from 'redux/interfaces';
|
||||
import reducer from 'redux/reducers/consumerGroups/reducer';
|
||||
import * as actions from 'redux/actions';
|
||||
|
||||
const state: ConsumerGroupsState = {
|
||||
byID: {
|
||||
test: {
|
||||
clusterId: 'local',
|
||||
consumerGroupId: 'test',
|
||||
},
|
||||
},
|
||||
allIDs: ['test'],
|
||||
};
|
||||
|
||||
describe('consumerGroup reducer', () => {
|
||||
describe('consumer group deletion', () => {
|
||||
it('correctly deletes a consumer group on deleteConsumerGroupAction.success', () => {
|
||||
expect(
|
||||
reducer(state, actions.deleteConsumerGroupAction.success('test'))
|
||||
).toEqual({
|
||||
byID: {},
|
||||
allIDs: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -34,6 +34,7 @@ const updateConsumerGroupsList = (
|
|||
};
|
||||
|
||||
const reducer = (state = initialState, action: Action): ConsumerGroupsState => {
|
||||
let newState;
|
||||
switch (action.type) {
|
||||
case getType(actions.fetchConsumerGroupsAction.success):
|
||||
return updateConsumerGroupsList(state, action.payload);
|
||||
|
@ -48,6 +49,11 @@ const reducer = (state = initialState, action: Action): ConsumerGroupsState => {
|
|||
},
|
||||
},
|
||||
};
|
||||
case getType(actions.deleteConsumerGroupAction.success):
|
||||
newState = { ...state };
|
||||
delete newState.byID[action.payload];
|
||||
newState.allIDs = newState.allIDs.filter((id) => id !== action.payload);
|
||||
return newState;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -21,12 +21,20 @@ const getConsumerGroupsListFetchingStatus = createFetchingSelector(
|
|||
const getConsumerGroupDetailsFetchingStatus = createFetchingSelector(
|
||||
'GET_CONSUMER_GROUP_DETAILS'
|
||||
);
|
||||
const getConsumerGroupDeletingStatus = createFetchingSelector(
|
||||
'DELETE_CONSUMER_GROUP'
|
||||
);
|
||||
|
||||
export const getIsConsumerGroupsListFetched = createSelector(
|
||||
getConsumerGroupsListFetchingStatus,
|
||||
(status) => status === 'fetched'
|
||||
);
|
||||
|
||||
export const getIsConsumerGroupsDeleted = createSelector(
|
||||
getConsumerGroupDeletingStatus,
|
||||
(status) => status === 'fetched'
|
||||
);
|
||||
|
||||
export const getIsConsumerGroupDetailsFetched = createSelector(
|
||||
getConsumerGroupDetailsFetchingStatus,
|
||||
(status) => status === 'fetched'
|
||||
|
|
Loading…
Add table
Reference in a new issue