Refactor TopicMessages's store to redux/toolkit (#1902)

* Refactor TopicMessages's store to redux/toolkit

* adding test cases to improve coverage

* fixing eslint error

* removing extra code

* Transform redux connect reducer and actions into toolkit slice. (#1883)

* integrate redux toolkit for connects

* uncomment test case

* remove unnecessary comment

* reduce duplication

* Implement code highlighting for smart filters (#1868)

* Implement code highlighting for smart filters

* delete unnecessary code

* fixing eslint problem

* fixing sonar code smells (#1826)

* fixing sonar code smells

* removing unnecessary change

* fixing some sonar code smell issues

* making requested changes

* Fix sonar badges in readme (#1906)

* fixing merge conflicts

Co-authored-by: Oleg Shur <workshur@gmail.com>

* Add positive notifications after some successful actions. (#1830)

* add some positive notifications after successful actions

* some improvements

* improve alerts reducer tests

* Fix test warnings (#1908)

* fix fetch mock warnings in brokers.spec.tsx

* use separate waitFor-s for fetch-mock call expects

* Persist show internal topics switch state (#1832)

* persist show internal topics switch state

* minor improvements in topics list tests

* prevent duplication in topic list test file

* fix seek type select border-radius and user-select (#1904)

* Refetch topics list to display the updated data (#1900)

* refetch topics list to display the updated data

* fixing sonar code smells (#1826)

* fixing sonar code smells

* removing unnecessary change

* fixing some sonar code smell issues

* making requested changes

* Fix sonar badges in readme (#1906)

Co-authored-by: Robert Azizbekyan <103438454+rAzizbekyan@users.noreply.github.com>
Co-authored-by: Oleg Shur <workshur@gmail.com>

* Show confirmation modal when clear messages is clicked in topics list (#1899)

* show confirmation modal when clear messages is clicked in topics list

* change variable name

* add missing dependencies

* use useModal hook for topics list modals

* display a human-readable error message for topic custom parameers value field (#1896)

* modify dependencies to fix partitions filter bug (#1901)

Co-authored-by: Arsen Simonyan <103444767+simonyandev@users.noreply.github.com>
Co-authored-by: Oleg Shur <workshur@gmail.com>
This commit is contained in:
Robert Azizbekyan 2022-05-12 16:41:58 +04:00 committed by GitHub
parent fa6f587f97
commit 81d1e955da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 463 additions and 376 deletions

File diff suppressed because it is too large Load diff

View file

@ -50,11 +50,11 @@ export interface TopicsListProps {
deleteTopics(topicName: TopicName, clusterNames: ClusterName[]): void;
recreateTopic(topicName: TopicName, clusterName: ClusterName): void;
clearTopicsMessages(topicName: TopicName, clusterNames: ClusterName[]): void;
clearTopicMessages(
topicName: TopicName,
clusterName: ClusterName,
partitions?: number[]
): void;
clearTopicMessages(params: {
topicName: TopicName;
clusterName: ClusterName;
partitions?: number[];
}): void;
search: string;
orderBy: TopicColumnsToSort | null;
sortOrder: SortOrder;
@ -225,7 +225,7 @@ const List: React.FC<TopicsListProps> = ({
}, [name]);
const clearTopicMessagesHandler = React.useCallback(() => {
clearTopicMessages(clusterName, name);
clearTopicMessages({ clusterName, topicName: name });
fetchTopicsList(topicsListParams);
closeClearMessagesModal();
}, [name, fetchTopicsList, topicsListParams]);

View file

@ -6,10 +6,10 @@ import {
deleteTopics,
recreateTopic,
clearTopicsMessages,
clearTopicMessages,
setTopicsSearchAction,
setTopicsOrderByAction,
} from 'redux/actions';
import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
import {
getTopicList,
getAreTopicsFetching,

View file

@ -298,10 +298,12 @@ describe('List', () => {
it('triggers the deleteTopics when clicked on the delete button', async () => {
await checkActionButtonClick('deleteTopics');
expect(mockDeleteTopics).toBeCalledTimes(1);
});
it('triggers the clearTopicsMessages when clicked on the clear button', async () => {
await checkActionButtonClick('clearTopicsMessages');
expect(mockClearTopicsMessages).toBeCalledTimes(1);
});
it('closes ConfirmationModal when clicked on the cancel button', async () => {

View file

@ -37,7 +37,10 @@ interface Props extends Topic, TopicDetails {
isDeletePolicy: boolean;
deleteTopic: (clusterName: ClusterName, topicName: TopicName) => void;
recreateTopic: (clusterName: ClusterName, topicName: TopicName) => void;
clearTopicMessages(clusterName: ClusterName, topicName: TopicName): void;
clearTopicMessages(params: {
clusterName: ClusterName;
topicName: TopicName;
}): void;
}
const HeaderControlsWrapper = styled.div`
@ -81,7 +84,7 @@ const Details: React.FC<Props> = ({
}, [isDeleted, clusterName, dispatch, history]);
const clearTopicMessagesHandler = React.useCallback(() => {
clearTopicMessages(clusterName, topicName);
clearTopicMessages({ clusterName, topicName });
setClearTopicConfirmationVisible(false);
}, [clusterName, topicName, clearTopicMessages]);

View file

@ -1,7 +1,8 @@
import { connect } from 'react-redux';
import { ClusterName, RootState, TopicName } from 'redux/interfaces';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { deleteTopic, clearTopicMessages, recreateTopic } from 'redux/actions';
import { deleteTopic, recreateTopic } from 'redux/actions';
import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
import {
getIsTopicDeleted,
getIsTopicDeletePolicy,

View file

@ -7,7 +7,7 @@ import {
updateTopicMessagesMeta,
updateTopicMessagesPhase,
setTopicMessagesFetchingStatus,
} from 'redux/actions';
} from 'redux/reducers/topicMessages/topicMessagesSlice';
import {
getTopicMessgesMeta,
getTopicMessgesPhase,

View file

@ -14,11 +14,11 @@ import { Tag } from 'components/common/Tag/Tag.styled';
export interface Props extends Topic, TopicDetails {
clusterName: ClusterName;
topicName: TopicName;
clearTopicMessages(
clusterName: ClusterName,
topicName: TopicName,
partitions?: number[]
): void;
clearTopicMessages(params: {
clusterName: ClusterName;
topicName: TopicName;
partitions?: number[];
}): void;
}
const Overview: React.FC<Props> = ({
@ -120,9 +120,11 @@ const Overview: React.FC<Props> = ({
<Dropdown label={<VerticalElipsisIcon />} right>
<DropdownItem
onClick={() =>
clearTopicMessages(clusterName, topicName, [
partition,
])
clearTopicMessages({
clusterName,
topicName,
partitions: [partition],
})
}
danger
>

View file

@ -2,7 +2,7 @@ import { connect } from 'react-redux';
import { RootState, TopicName, ClusterName } from 'redux/interfaces';
import { getTopicByName } from 'redux/reducers/topics/selectors';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { clearTopicMessages } from 'redux/actions';
import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
import Overview from 'components/Topics/Topic/Details/Overview/Overview';
interface RouteProps {

View file

@ -127,10 +127,10 @@ describe('Details', () => {
const submitButton = screen.getAllByText('Submit')[0];
userEvent.click(submitButton);
expect(mockClearTopicMessages).toHaveBeenCalledWith(
mockClusterName,
internalTopicPayload.name
);
expect(mockClearTopicMessages).toHaveBeenCalledWith({
clusterName: mockClusterName,
topicName: internalTopicPayload.name,
});
});
it('closes the modal when cancel button is clicked', () => {

View file

@ -1,6 +1,7 @@
import { connect } from 'react-redux';
import { RootState } from 'redux/interfaces';
import { fetchTopicDetails, resetTopicMessages } from 'redux/actions';
import { fetchTopicDetails } from 'redux/actions';
import { resetTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
import { getIsTopicDetailsFetching } from 'redux/reducers/topics/selectors';
import Topic from './Topic';

View file

@ -5,10 +5,6 @@ import {
TopicMessageSchema,
} from 'generated-sources';
import { FailurePayload } from 'redux/interfaces';
import {
topicMessagePayload,
topicMessagesMetaPayload,
} from 'redux/reducers/topicMessages/__test__/fixtures';
import { mockTopicsState } from './fixtures';
@ -85,39 +81,6 @@ describe('Actions', () => {
});
});
describe('topic messages', () => {
it('creates ADD_TOPIC_MESSAGE', () => {
expect(actions.addTopicMessage({ message: topicMessagePayload })).toEqual(
{
type: 'ADD_TOPIC_MESSAGE',
payload: { message: topicMessagePayload },
}
);
});
it('creates RESET_TOPIC_MESSAGES', () => {
expect(actions.resetTopicMessages()).toEqual({
type: 'RESET_TOPIC_MESSAGES',
});
});
it('creates UPDATE_TOPIC_MESSAGES_PHASE', () => {
expect(actions.updateTopicMessagesPhase('Polling')).toEqual({
type: 'UPDATE_TOPIC_MESSAGES_PHASE',
payload: 'Polling',
});
});
it('creates UPDATE_TOPIC_MESSAGES_META', () => {
expect(actions.updateTopicMessagesMeta(topicMessagesMetaPayload)).toEqual(
{
type: 'UPDATE_TOPIC_MESSAGES_META',
payload: topicMessagesMetaPayload,
}
);
});
});
describe('sending messages', () => {
describe('fetchTopicMessageSchemaAction', () => {
it('creates GET_TOPIC_SCHEMA__REQUEST', () => {

View file

@ -83,38 +83,6 @@ describe('Thunks', () => {
});
});
describe('clearTopicMessages', () => {
it('creates CLEAR_TOPIC_MESSAGES__SUCCESS when deleting existing messages', async () => {
fetchMock.deleteOnce(
`/api/clusters/${clusterName}/topics/${topicName}/messages`,
200
);
await store.dispatch(thunks.clearTopicMessages(clusterName, topicName));
expect(getTypeAndPayload(store)).toEqual([
actions.clearMessagesTopicAction.request(),
actions.clearMessagesTopicAction.success(),
...getAlertActions(store),
]);
});
it('creates CLEAR_TOPIC_MESSAGES__FAILURE when deleting existing messages', async () => {
fetchMock.deleteOnce(
`/api/clusters/${clusterName}/topics/${topicName}/messages`,
404
);
try {
await store.dispatch(thunks.clearTopicMessages(clusterName, topicName));
} catch (error) {
const err = error as Response;
expect(err.status).toEqual(404);
expect(store.getActions()).toEqual([
actions.clearMessagesTopicAction.request(),
actions.clearMessagesTopicAction.failure({}),
]);
}
});
});
describe('fetchTopicConsumerGroups', () => {
it('GET_TOPIC_CONSUMER_GROUPS__FAILURE', async () => {
fetchMock.getOnce(

View file

@ -3,8 +3,6 @@ import { FailurePayload, TopicName, TopicsState } from 'redux/interfaces';
import {
TopicColumnsToSort,
Topic,
TopicMessage,
TopicMessageConsuming,
TopicMessageSchema,
} from 'generated-sources';
@ -73,25 +71,6 @@ export const fetchTopicConsumerGroupsAction = createAsyncAction(
'GET_TOPIC_CONSUMER_GROUPS__FAILURE'
)<undefined, TopicsState, undefined>();
export const addTopicMessage = createAction('ADD_TOPIC_MESSAGE')<{
message: TopicMessage;
prepend?: boolean;
}>();
export const resetTopicMessages = createAction('RESET_TOPIC_MESSAGES')();
export const setTopicMessagesFetchingStatus = createAction(
'SET_TOPIC_MESSAGES_FETCHING_STATUS'
)<boolean>();
export const updateTopicMessagesPhase = createAction(
'UPDATE_TOPIC_MESSAGES_PHASE'
)<string>();
export const updateTopicMessagesMeta = createAction(
'UPDATE_TOPIC_MESSAGES_META'
)<TopicMessageConsuming>();
export const fetchTopicMessageSchemaAction = createAsyncAction(
'GET_TOPIC_SCHEMA__REQUEST',
'GET_TOPIC_SCHEMA__SUCCESS',

View file

@ -21,6 +21,7 @@ import {
TopicFormData,
AppDispatch,
} from 'redux/interfaces';
import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
import { BASE_PARAMS } from 'lib/constants';
import * as actions from 'redux/actions/actions';
import { getResponse } from 'lib/errorHandling';
@ -63,44 +64,12 @@ export const fetchTopicsList =
dispatch(actions.fetchTopicsListAction.failure());
}
};
export const clearTopicMessages =
(
clusterName: ClusterName,
topicName: TopicName,
partitions?: number[]
): PromiseThunkResult =>
async (dispatch) => {
dispatch(actions.clearMessagesTopicAction.request());
try {
await messagesApiClient.deleteTopicMessages({
clusterName,
topicName,
partitions,
});
dispatch(actions.clearMessagesTopicAction.success());
(dispatch as AppDispatch)(
showSuccessAlert({
id: `message-${topicName}-${clusterName}-${partitions}`,
message: 'Messages successfully cleared!',
})
);
} catch (e) {
const response = await getResponse(e);
const alert: FailurePayload = {
subject: [clusterName, topicName, partitions].join('-'),
title: `Clear Topic Messages`,
response,
};
dispatch(actions.clearMessagesTopicAction.failure({ alert }));
}
};
export const clearTopicsMessages =
(clusterName: ClusterName, topicsName: TopicName[]): PromiseThunkResult =>
async (dispatch) => {
topicsName.forEach((topicName) => {
dispatch(clearTopicMessages(clusterName, topicName));
dispatch(clearTopicMessages({ clusterName, topicName }));
});
};

View file

@ -7,7 +7,7 @@ import schemas from 'redux/reducers/schemas/schemasSlice';
import connect from 'redux/reducers/connect/connectSlice';
import topics from './topics/reducer';
import topicMessages from './topicMessages/reducer';
import topicMessages from './topicMessages/topicMessagesSlice';
import consumerGroups from './consumerGroups/consumerGroupsSlice';
import ksqlDb from './ksqlDb/ksqlDbSlice';
import legacyLoader from './loader/reducer';

View file

@ -1,10 +1,10 @@
import {
import reducer, {
addTopicMessage,
clearTopicMessages,
resetTopicMessages,
updateTopicMessagesMeta,
updateTopicMessagesPhase,
} from 'redux/actions';
import reducer from 'redux/reducers/topicMessages/reducer';
} from 'redux/reducers/topicMessages/topicMessagesSlice';
import {
topicMessagePayload,
@ -12,6 +12,9 @@ import {
topicMessagesMetaPayload,
} from './fixtures';
const clusterName = 'local';
const topicName = 'localTopic';
describe('TopicMessages reducer', () => {
it('Adds new message', () => {
const state = reducer(
@ -53,7 +56,7 @@ describe('TopicMessages reducer', () => {
]);
});
it('Clears messages', () => {
it('reset messages', () => {
const state = reducer(
undefined,
addTopicMessage({ message: topicMessagePayload })
@ -63,6 +66,25 @@ describe('TopicMessages reducer', () => {
const newState = reducer(state, resetTopicMessages());
expect(newState.messages.length).toEqual(0);
});
it('clear messages', () => {
const state = reducer(
undefined,
addTopicMessage({ message: topicMessagePayload })
);
expect(state.messages.length).toEqual(1);
expect(
reducer(state, {
type: clearTopicMessages.fulfilled,
payload: { clusterName, topicName },
})
).toEqual({
...state,
messages: [],
});
});
it('Updates Topic Messages Phase', () => {
const phase = 'Polling';

View file

@ -1,11 +1,11 @@
import { store } from 'redux/store';
import * as selectors from 'redux/reducers/topicMessages/selectors';
import { initialState } from 'redux/reducers/topicMessages/reducer';
import {
initialState,
addTopicMessage,
updateTopicMessagesMeta,
updateTopicMessagesPhase,
} from 'redux/actions';
} from 'redux/reducers/topicMessages/topicMessagesSlice';
import { topicMessagePayload, topicMessagesMetaPayload } from './fixtures';

View file

@ -1,57 +0,0 @@
import { Action, TopicMessagesState } from 'redux/interfaces';
import { getType } from 'typesafe-actions';
import * as actions from 'redux/actions';
import { TopicMessage } from 'generated-sources';
export const initialState: TopicMessagesState = {
messages: [],
meta: {
bytesConsumed: 0,
elapsedMs: 0,
messagesConsumed: 0,
isCancelled: false,
},
isFetching: false,
};
// eslint-disable-next-line @typescript-eslint/default-param-last
const reducer = (state = initialState, action: Action): TopicMessagesState => {
switch (action.type) {
case getType(actions.addTopicMessage): {
const messages: TopicMessage[] = action.payload.prepend
? [action.payload.message, ...state.messages]
: [...state.messages, action.payload.message];
return {
...state,
messages,
};
}
case getType(actions.resetTopicMessages):
return initialState;
case getType(actions.updateTopicMessagesPhase):
return {
...state,
phase: action.payload,
};
case getType(actions.updateTopicMessagesMeta):
return {
...state,
meta: action.payload,
};
case getType(actions.setTopicMessagesFetchingStatus):
return {
...state,
isFetching: action.payload,
};
case getType(actions.clearMessagesTopicAction.success):
return {
...state,
messages: [],
};
default:
return state;
}
};
export default reducer;

View file

@ -0,0 +1,92 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { TopicMessagesState, ClusterName, TopicName } from 'redux/interfaces';
import { TopicMessage, Configuration, MessagesApi } from 'generated-sources';
import { BASE_PARAMS } from 'lib/constants';
import { getResponse } from 'lib/errorHandling';
import { showSuccessAlert } from 'redux/reducers/alerts/alertsSlice';
const apiClientConf = new Configuration(BASE_PARAMS);
export const messagesApiClient = new MessagesApi(apiClientConf);
export const clearTopicMessages = createAsyncThunk<
undefined,
{ clusterName: ClusterName; topicName: TopicName; partitions?: number[] }
>(
'topicMessages/clearTopicMessages',
async (
{ clusterName, topicName, partitions },
{ rejectWithValue, dispatch }
) => {
try {
await messagesApiClient.deleteTopicMessages({
clusterName,
topicName,
partitions,
});
dispatch(
showSuccessAlert({
id: `message-${topicName}-${clusterName}-${partitions}`,
message: 'Messages successfully cleared!',
})
);
return undefined;
} catch (err) {
return rejectWithValue(await getResponse(err as Response));
}
}
);
export const initialState: TopicMessagesState = {
messages: [],
meta: {
bytesConsumed: 0,
elapsedMs: 0,
messagesConsumed: 0,
isCancelled: false,
},
isFetching: false,
};
export const topicMessagesSlice = createSlice({
name: 'topicMessages',
initialState,
reducers: {
addTopicMessage: (state, action) => {
const messages: TopicMessage[] = action.payload.prepend
? [action.payload.message, ...state.messages]
: [...state.messages, action.payload.message];
return {
...state,
messages,
};
},
resetTopicMessages: () => initialState,
updateTopicMessagesPhase: (state, action) => {
state.phase = action.payload;
},
updateTopicMessagesMeta: (state, action) => {
state.meta = action.payload;
},
setTopicMessagesFetchingStatus: (state, action) => {
state.isFetching = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(clearTopicMessages.fulfilled, (state) => {
state.messages = [];
});
},
});
export const {
addTopicMessage,
resetTopicMessages,
updateTopicMessagesPhase,
updateTopicMessagesMeta,
setTopicMessagesFetchingStatus,
} = topicMessagesSlice.actions;
export default topicMessagesSlice.reducer;