Bladeren bron

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

* add some positive notifications after successful actions

* some improvements

* improve alerts reducer tests
Arsen Simonyan 3 jaren geleden
bovenliggende
commit
bd9292e8a9

+ 1 - 24
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<TasksProps> = ({
-  fetchTasks,
-  areTasksFetching,
-  tasks,
-}) => {
-  const { clusterName, connectName, connectorName } = useParams<RouterParams>();
-
-  React.useEffect(() => {
-    fetchTasks({ clusterName, connectName, connectorName });
-  }, [fetchTasks, clusterName, connectName, connectorName]);
-
+const Tasks: React.FC<TasksProps> = ({ areTasksFetching, tasks }) => {
   if (areTasksFetching) {
     return <PageLoader />;
   }

+ 1 - 18
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 }}
         >
-          <Tasks
-            fetchTasks={jest.fn()}
-            areTasksFetching={false}
-            tasks={tasks}
-            {...props}
-          />
+          <Tasks areTasksFetching={false} tasks={tasks} {...props} />
         </TestRouterWrapper>
       </ThemeProvider>
     );
@@ -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,
-      });
-    });
   });
 });

+ 5 - 0
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')
+  );

+ 5 - 2
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),
       ]);
     });
 

+ 16 - 0
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());
     }

+ 38 - 0
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);
+    });
+  });
 });

+ 28 - 0
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<RootState>(
 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;

+ 9 - 2
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 },

+ 17 - 2
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));