From bd9292e8a9e554850d4d8ec655ea3054b250992f Mon Sep 17 00:00:00 2001 From: Arsen Simonyan <103444767+simonyandev@users.noreply.github.com> Date: Wed, 27 Apr 2022 18:03:14 +0400 Subject: [PATCH] Add positive notifications after some successful actions. (#1830) * add some positive notifications after successful actions * some improvements * improve alerts reducer tests --- .../Connect/Details/Tasks/Tasks.tsx | 25 +----------- .../Details/Tasks/__tests__/Tasks.spec.tsx | 19 +--------- kafka-ui-react-app/src/lib/testHelpers.tsx | 5 +++ .../actions/__test__/thunks/topics.spec.ts | 7 +++- .../src/redux/actions/thunks/topics.ts | 16 ++++++++ .../reducers/alerts/__test__/reducer.spec.ts | 38 +++++++++++++++++++ .../src/redux/reducers/alerts/alertsSlice.ts | 28 ++++++++++++++ .../reducers/connect/__test__/reducer.spec.ts | 11 +++++- .../redux/reducers/connect/connectSlice.ts | 19 +++++++++- 9 files changed, 120 insertions(+), 48 deletions(-) diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx b/kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx index 09c6cff3bc..0cd14ecd70 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx @@ -1,40 +1,17 @@ import React from 'react'; -import { useParams } from 'react-router'; import { Task } from 'generated-sources'; -import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces'; import PageLoader from 'components/common/PageLoader/PageLoader'; import { Table } from 'components/common/table/Table/Table.styled'; import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; import ListItemContainer from './ListItem/ListItemContainer'; -interface RouterParams { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; -} - export interface TasksProps { - fetchTasks(payload: { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - }): void; areTasksFetching: boolean; tasks: Task[]; } -const Tasks: React.FC = ({ - fetchTasks, - areTasksFetching, - tasks, -}) => { - const { clusterName, connectName, connectorName } = useParams(); - - React.useEffect(() => { - fetchTasks({ clusterName, connectName, connectorName }); - }, [fetchTasks, clusterName, connectName, connectorName]); - +const Tasks: React.FC = ({ areTasksFetching, tasks }) => { if (areTasksFetching) { return ; } diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx index 245d7687a5..e5467d0ba2 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { create } from 'react-test-renderer'; -import { mount } from 'enzyme'; import { containerRendersView, TestRouterWrapper } from 'lib/testHelpers'; import { clusterConnectConnectorTasksPath } from 'lib/paths'; import TasksContainer from 'components/Connect/Details/Tasks/TasksContainer'; @@ -35,12 +34,7 @@ describe('Tasks', () => { pathname={pathname} urlParams={{ clusterName, connectName, connectorName }} > - + ); @@ -59,16 +53,5 @@ describe('Tasks', () => { const wrapper = create(setupWrapper({ tasks: [] })); expect(wrapper.toJSON()).toMatchSnapshot(); }); - - it('fetches tasks on mount', () => { - const fetchTasks = jest.fn(); - mount(setupWrapper({ fetchTasks })); - expect(fetchTasks).toHaveBeenCalledTimes(1); - expect(fetchTasks).toHaveBeenCalledWith({ - clusterName, - connectName, - connectorName, - }); - }); }); }); diff --git a/kafka-ui-react-app/src/lib/testHelpers.tsx b/kafka-ui-react-app/src/lib/testHelpers.tsx index cad78ffa9d..37c2a975e8 100644 --- a/kafka-ui-react-app/src/lib/testHelpers.tsx +++ b/kafka-ui-react-app/src/lib/testHelpers.tsx @@ -126,3 +126,8 @@ export class EventSourceMock { export const getTypeAndPayload = (store: typeof mockStoreCreator) => { return store.getActions().map(({ type, payload }) => ({ type, payload })); }; + +export const getAlertActions = (mockStore: typeof mockStoreCreator) => + getTypeAndPayload(mockStore).filter((currentAction: AnyAction) => + currentAction.type.startsWith('alerts') + ); diff --git a/kafka-ui-react-app/src/redux/actions/__test__/thunks/topics.spec.ts b/kafka-ui-react-app/src/redux/actions/__test__/thunks/topics.spec.ts index 51426f6d67..b4642caabf 100644 --- a/kafka-ui-react-app/src/redux/actions/__test__/thunks/topics.spec.ts +++ b/kafka-ui-react-app/src/redux/actions/__test__/thunks/topics.spec.ts @@ -7,6 +7,7 @@ import { MessageSchemaSourceEnum, TopicMessageSchema } from 'generated-sources'; import { FailurePayload } from 'redux/interfaces'; import { getResponse } from 'lib/errorHandling'; import { internalTopicPayload } from 'redux/reducers/topics/__test__/fixtures'; +import { getAlertActions, getTypeAndPayload } from 'lib/testHelpers'; const store = mockStoreCreator; @@ -57,9 +58,10 @@ describe('Thunks', () => { internalTopicPayload ); await store.dispatch(thunks.recreateTopic(clusterName, topicName)); - expect(store.getActions()).toEqual([ + expect(getTypeAndPayload(store)).toEqual([ actions.recreateTopicAction.request(), actions.recreateTopicAction.success(internalTopicPayload), + ...getAlertActions(store), ]); }); @@ -88,9 +90,10 @@ describe('Thunks', () => { 200 ); await store.dispatch(thunks.clearTopicMessages(clusterName, topicName)); - expect(store.getActions()).toEqual([ + expect(getTypeAndPayload(store)).toEqual([ actions.clearMessagesTopicAction.request(), actions.clearMessagesTopicAction.success(), + ...getAlertActions(store), ]); }); diff --git a/kafka-ui-react-app/src/redux/actions/thunks/topics.ts b/kafka-ui-react-app/src/redux/actions/thunks/topics.ts index 00aa1ea014..bf15a2579f 100644 --- a/kafka-ui-react-app/src/redux/actions/thunks/topics.ts +++ b/kafka-ui-react-app/src/redux/actions/thunks/topics.ts @@ -19,10 +19,12 @@ import { TopicsState, FailurePayload, TopicFormData, + AppDispatch, } from 'redux/interfaces'; import { BASE_PARAMS } from 'lib/constants'; import * as actions from 'redux/actions/actions'; import { getResponse } from 'lib/errorHandling'; +import { showSuccessAlert } from 'redux/reducers/alerts/alertsSlice'; const apiClientConf = new Configuration(BASE_PARAMS); export const topicsApiClient = new TopicsApi(apiClientConf); @@ -76,6 +78,13 @@ export const clearTopicMessages = 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 = { @@ -269,6 +278,13 @@ export const recreateTopic = topicName, }); dispatch(actions.recreateTopicAction.success(topic)); + + (dispatch as AppDispatch)( + showSuccessAlert({ + id: topicName, + message: 'Topic successfully recreated!', + }) + ); } catch (e) { dispatch(actions.recreateTopicAction.failure()); } diff --git a/kafka-ui-react-app/src/redux/reducers/alerts/__test__/reducer.spec.ts b/kafka-ui-react-app/src/redux/reducers/alerts/__test__/reducer.spec.ts index 10616a7616..d0880a76b6 100644 --- a/kafka-ui-react-app/src/redux/reducers/alerts/__test__/reducer.spec.ts +++ b/kafka-ui-react-app/src/redux/reducers/alerts/__test__/reducer.spec.ts @@ -1,8 +1,12 @@ import { dismissAlert, createTopicAction } from 'redux/actions'; import reducer from 'redux/reducers/alerts/reducer'; +import { showSuccessAlert } from 'redux/reducers/alerts/alertsSlice'; +import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator'; import { failurePayload1, failurePayload2 } from './fixtures'; +const store = mockStoreCreator; + jest.mock('lodash', () => ({ ...jest.requireActual('lodash'), now: () => 1234567890, @@ -63,4 +67,38 @@ describe('Alerts reducer', () => { }, }); }); + + describe('Alert thunks', () => { + afterEach(() => { + store.clearActions(); + }); + + it('dismisses alert after showing success alert', async () => { + const passedPayload = { id: 'some-id', message: 'Alert message.' }; + + const { payload: creationDate } = await store.dispatch( + showSuccessAlert(passedPayload) + ); + + const actionsData = store + .getActions() + .map(({ type, payload }) => ({ type, payload })); + + const expectedActions = [ + { type: 'alerts/showSuccessAlert/pending', payload: undefined }, + { + type: 'alerts/alertAdded', + payload: { + ...passedPayload, + title: '', + type: 'success', + createdAt: creationDate, + }, + }, + { type: 'alerts/showSuccessAlert/fulfilled', payload: creationDate }, + ]; + + expect(actionsData).toEqual(expectedActions); + }); + }); }); diff --git a/kafka-ui-react-app/src/redux/reducers/alerts/alertsSlice.ts b/kafka-ui-react-app/src/redux/reducers/alerts/alertsSlice.ts index aef3a2e9ec..938965ad3c 100644 --- a/kafka-ui-react-app/src/redux/reducers/alerts/alertsSlice.ts +++ b/kafka-ui-react-app/src/redux/reducers/alerts/alertsSlice.ts @@ -1,4 +1,5 @@ import { + createAsyncThunk, createEntityAdapter, createSlice, nanoid, @@ -69,4 +70,31 @@ export const { selectAll } = alertsAdapter.getSelectors( export const { alertDissmissed, alertAdded, serverErrorAlertAdded } = alertsSlice.actions; +export const showSuccessAlert = createAsyncThunk< + number, + { id: string; message: string }, + { fulfilledMeta: null } +>( + 'alerts/showSuccessAlert', + async ({ id, message }, { dispatch, fulfillWithValue }) => { + const creationDate = Date.now(); + + dispatch( + alertAdded({ + id, + message, + title: '', + type: 'success', + createdAt: creationDate, + }) + ); + + setTimeout(() => { + dispatch(alertDissmissed(id)); + }, 3000); + + return fulfillWithValue(creationDate, null); + } +); + export default alertsSlice.reducer; diff --git a/kafka-ui-react-app/src/redux/reducers/connect/__test__/reducer.spec.ts b/kafka-ui-react-app/src/redux/reducers/connect/__test__/reducer.spec.ts index f174605a35..3630753b71 100644 --- a/kafka-ui-react-app/src/redux/reducers/connect/__test__/reducer.spec.ts +++ b/kafka-ui-react-app/src/redux/reducers/connect/__test__/reducer.spec.ts @@ -21,7 +21,7 @@ import reducer, { } from 'redux/reducers/connect/connectSlice'; import fetchMock from 'fetch-mock-jest'; import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator'; -import { getTypeAndPayload } from 'lib/testHelpers'; +import { getTypeAndPayload, getAlertActions } from 'lib/testHelpers'; import { connects, @@ -636,6 +636,11 @@ describe('Connect slice', () => { expect(getTypeAndPayload(store)).toEqual([ { type: restartConnectorTask.pending.type }, { type: fetchConnectorTasks.pending.type }, + { + type: fetchConnectorTasks.fulfilled.type, + payload: { tasks }, + }, + ...getAlertActions(store), { type: restartConnectorTask.fulfilled.type }, ]); }); @@ -710,6 +715,7 @@ describe('Connect slice', () => { `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`, connectorServerPayload ); + await store.dispatch( updateConnectorConfig({ clusterName, @@ -719,7 +725,8 @@ describe('Connect slice', () => { }) ); expect(getTypeAndPayload(store)).toEqual([ - { type: updateConnectorConfig.pending.type }, + { type: updateConnectorConfig.pending.type, payload: undefined }, + ...getAlertActions(store), { type: updateConnectorConfig.fulfilled.type, payload: { connector }, diff --git a/kafka-ui-react-app/src/redux/reducers/connect/connectSlice.ts b/kafka-ui-react-app/src/redux/reducers/connect/connectSlice.ts index 09a90c3c0d..9402b3c2e8 100644 --- a/kafka-ui-react-app/src/redux/reducers/connect/connectSlice.ts +++ b/kafka-ui-react-app/src/redux/reducers/connect/connectSlice.ts @@ -22,6 +22,7 @@ import { ConnectorSearch, ConnectState, } from 'redux/interfaces'; +import { showSuccessAlert } from 'redux/reducers/alerts/alertsSlice'; const apiClientConf = new Configuration(BASE_PARAMS); export const kafkaConnectApiClient = new KafkaConnectApi(apiClientConf); @@ -254,7 +255,7 @@ export const restartConnectorTask = createAsyncThunk< taskId: Number(taskId), }); - dispatch( + await dispatch( fetchConnectorTasks({ clusterName, connectName, @@ -262,6 +263,13 @@ export const restartConnectorTask = createAsyncThunk< }) ); + dispatch( + showSuccessAlert({ + id: `connect-${connectName}-${clusterName}`, + message: 'Tasks successfully restarted.', + }) + ); + return undefined; } catch (err) { return rejectWithValue(await getResponse(err as Response)); @@ -305,7 +313,7 @@ export const updateConnectorConfig = createAsyncThunk< 'connect/updateConnectorConfig', async ( { clusterName, connectName, connectorName, connectorConfig }, - { rejectWithValue } + { rejectWithValue, dispatch } ) => { try { const connector = await kafkaConnectApiClient.setConnectorConfig({ @@ -315,6 +323,13 @@ export const updateConnectorConfig = createAsyncThunk< requestBody: connectorConfig, }); + dispatch( + showSuccessAlert({ + id: `connector-${connectorName}-${clusterName}`, + message: 'Connector config updated.', + }) + ); + return { connector }; } catch (err) { return rejectWithValue(await getResponse(err as Response));