Add positive notifications after some successful actions. (#1830)
* add some positive notifications after successful actions * some improvements * improve alerts reducer tests
This commit is contained in:
parent
3275b3fb94
commit
bd9292e8a9
9 changed files with 120 additions and 48 deletions
|
@ -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,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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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')
|
||||
);
|
||||
|
|
|
@ -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),
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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));
|
||||
|
|
Loading…
Add table
Reference in a new issue