Use react-hot-toaster for alerts (#2312)

* Use react-hot-toaster for alerts

* Fix linting problems
This commit is contained in:
Oleg Shur 2022-07-25 14:16:00 +03:00 committed by GitHub
parent 48325bc5ad
commit bffe316063
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 237 additions and 394 deletions

View file

@ -34,6 +34,7 @@
"react-datepicker": "^4.8.0", "react-datepicker": "^4.8.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"react-hook-form": "7.6.9", "react-hook-form": "7.6.9",
"react-hot-toast": "^2.3.0",
"react-is": "^18.2.0", "react-is": "^18.2.0",
"react-multi-select-component": "^4.0.6", "react-multi-select-component": "^4.0.6",
"react-redux": "^8.0.2", "react-redux": "^8.0.2",

View file

@ -70,6 +70,7 @@ specifiers:
react-datepicker: ^4.8.0 react-datepicker: ^4.8.0
react-dom: ^18.1.0 react-dom: ^18.1.0
react-hook-form: 7.6.9 react-hook-form: 7.6.9
react-hot-toast: ^2.3.0
react-is: ^18.2.0 react-is: ^18.2.0
react-multi-select-component: ^4.0.6 react-multi-select-component: ^4.0.6
react-redux: ^8.0.2 react-redux: ^8.0.2
@ -119,6 +120,7 @@ dependencies:
react-datepicker: 4.8.0_ef5jwxihqo6n7gxfmzogljlgcm react-datepicker: 4.8.0_ef5jwxihqo6n7gxfmzogljlgcm
react-dom: 18.1.0_react@18.1.0 react-dom: 18.1.0_react@18.1.0
react-hook-form: 7.6.9_react@18.1.0 react-hook-form: 7.6.9_react@18.1.0
react-hot-toast: 2.3.0_ef5jwxihqo6n7gxfmzogljlgcm
react-is: 18.2.0 react-is: 18.2.0
react-multi-select-component: 4.0.6_react@18.1.0 react-multi-select-component: 4.0.6_react@18.1.0
react-redux: 8.0.2_nfqigfgwurfoimtkde74cji6ga react-redux: 8.0.2_nfqigfgwurfoimtkde74cji6ga
@ -4957,6 +4959,12 @@ packages:
/globrex/0.1.2: /globrex/0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
/goober/2.1.10:
resolution: {integrity: sha512-7PpuQMH10jaTWm33sQgBQvz45pHR8N4l3Cu3WMGEWmHShAcTuuP7I+5/DwKo39fwti5A80WAjvqgz6SSlgWmGA==}
peerDependencies:
csstype: ^3.0.10
dev: false
/graceful-fs/4.2.10: /graceful-fs/4.2.10:
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
@ -6632,6 +6640,20 @@ packages:
react: 18.1.0 react: 18.1.0
dev: false dev: false
/react-hot-toast/2.3.0_ef5jwxihqo6n7gxfmzogljlgcm:
resolution: {integrity: sha512-/RxV+bfjld7tSJR1SCLzMAXgFuNW7fCpK6+vbYqfmbGSWcqTMz2rizrvfWKvtcPH5HK0NqxmBaC5SrAy1F42zA==}
engines: {node: '>=10'}
peerDependencies:
react: '>=16'
react-dom: '>=16'
dependencies:
goober: 2.1.10
react: 18.1.0
react-dom: 18.1.0_react@18.1.0
transitivePeerDependencies:
- csstype
dev: false
/react-is/16.13.1: /react-is/16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}

View file

@ -1,28 +0,0 @@
import React from 'react';
import { alertDissmissed, selectAll } from 'redux/reducers/alerts/alertsSlice';
import { useAppSelector, useAppDispatch } from 'lib/hooks/redux';
import Alert from 'components/Alerts/Alert';
const Alerts: React.FC = () => {
const alerts = useAppSelector(selectAll);
const dispatch = useAppDispatch();
const dismiss = (id: string) => () => {
dispatch(alertDissmissed(id));
};
return (
<>
{alerts.map(({ id, type, title, message }) => (
<Alert
key={id}
type={type}
title={title}
message={message}
onDissmiss={dismiss(id)}
/>
))}
</>
);
};
export default Alerts;

View file

@ -1,49 +0,0 @@
import React from 'react';
import { ServerResponse } from 'redux/interfaces';
import { act, screen } from '@testing-library/react';
import Alerts from 'components/Alerts/Alerts';
import { render } from 'lib/testHelpers';
import { store } from 'redux/store';
import { UnknownAsyncThunkRejectedWithValueAction } from '@reduxjs/toolkit/dist/matchers';
import userEvent from '@testing-library/user-event';
const payload: ServerResponse = {
status: 422,
statusText: 'Unprocessable Entity',
message: 'Unprocessable Entity',
url: 'https://test.com/clusters',
};
const action: UnknownAsyncThunkRejectedWithValueAction = {
type: 'any/action/rejected',
payload,
meta: {
arg: 'test',
requestId: 'test-request-id',
requestStatus: 'rejected',
aborted: false,
condition: false,
rejectedWithValue: true,
},
error: { message: 'Rejected' },
};
describe('Alerts', () => {
it('renders alerts', async () => {
store.dispatch(action);
await act(() => {
render(<Alerts />, { store });
});
expect(screen.getAllByRole('alert').length).toEqual(1);
const dissmissAlertButtons = screen.getAllByRole('button');
expect(dissmissAlertButtons.length).toEqual(1);
const dissmissButton = dissmissAlertButtons[0];
userEvent.click(dissmissButton);
expect(screen.queryAllByRole('alert').length).toEqual(0);
});
});

View file

@ -44,7 +44,9 @@ export const Sidebar = styled.div<{ $visible: boolean }>(
background: ${theme.menu.backgroundColor.normal}; background: ${theme.menu.backgroundColor.normal};
@media screen and (max-width: 1023px) { @media screen and (max-width: 1023px) {
${$visible && ${$visible &&
`transform: translate3d(${theme.layout.navBarWidth}, 0, 0)`}; css`
transform: translate3d(${theme.layout.navBarWidth}, 0, 0);
`};
left: -${theme.layout.navBarWidth}; left: -${theme.layout.navBarWidth};
z-index: 100; z-index: 100;
} }
@ -234,19 +236,6 @@ export const Hyperlink = styled(Link)(
` `
); );
export const AlertsContainer = styled.div`
max-width: 40%;
width: 500px;
position: fixed;
bottom: 15px;
right: 15px;
z-index: 1000;
@media screen and (max-width: 1023px) {
max-width: initial;
}
`;
export const LogoutButton = styled(Button)( export const LogoutButton = styled(Button)(
({ theme }) => css` ({ theme }) => css`
color: ${theme.button.primary.invertedColors.normal}; color: ${theme.button.primary.invertedColors.normal};

View file

@ -7,10 +7,11 @@ import PageLoader from 'components/common/PageLoader/PageLoader';
import Dashboard from 'components/Dashboard/Dashboard'; import Dashboard from 'components/Dashboard/Dashboard';
import ClusterPage from 'components/Cluster/Cluster'; import ClusterPage from 'components/Cluster/Cluster';
import Version from 'components/Version/Version'; import Version from 'components/Version/Version';
import Alerts from 'components/Alerts/Alerts';
import { ThemeProvider } from 'styled-components'; import { ThemeProvider } from 'styled-components';
import theme from 'theme/theme'; import theme from 'theme/theme';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { showServerError } from 'lib/errorHandling';
import { Toaster } from 'react-hot-toast';
import * as S from './App.styled'; import * as S from './App.styled';
import Logo from './common/Logo/Logo'; import Logo from './common/Logo/Logo';
@ -22,6 +23,11 @@ const queryClient = new QueryClient({
queries: { queries: {
suspense: true, suspense: true,
}, },
mutations: {
onError(error) {
showServerError(error as Response);
},
},
}, },
}); });
@ -113,9 +119,7 @@ const App: React.FC = () => {
/> />
</Routes> </Routes>
</S.Container> </S.Container>
<S.AlertsContainer role="toolbar"> <Toaster position="bottom-right" />
<Alerts />
</S.AlertsContainer>
</S.Layout> </S.Layout>
</ThemeProvider> </ThemeProvider>
</QueryClientProvider> </QueryClientProvider>

View file

@ -8,17 +8,14 @@ import {
import { getKsqlExecution } from 'redux/reducers/ksqlDb/selectors'; import { getKsqlExecution } from 'redux/reducers/ksqlDb/selectors';
import { BASE_PARAMS } from 'lib/constants'; import { BASE_PARAMS } from 'lib/constants';
import { KsqlResponse, KsqlTableResponse } from 'generated-sources'; import { KsqlResponse, KsqlTableResponse } from 'generated-sources';
import { alertAdded, alertDissmissed } from 'redux/reducers/alerts/alertsSlice';
import now from 'lodash/now';
import { ClusterNameRoute } from 'lib/paths'; import { ClusterNameRoute } from 'lib/paths';
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
import { showAlert, showSuccessAlert } from 'lib/errorHandling';
import type { FormValues } from './QueryForm/QueryForm'; import type { FormValues } from './QueryForm/QueryForm';
import * as S from './Query.styled'; import * as S from './Query.styled';
import QueryForm from './QueryForm/QueryForm'; import QueryForm from './QueryForm/QueryForm';
const AUTO_DISMISS_TIME = 8_000;
export const getFormattedErrorFromTableData = ( export const getFormattedErrorFromTableData = (
responseValues: KsqlTableResponse['values'] responseValues: KsqlTableResponse['values']
): { title: string; message: string } => { ): { title: string; message: string } => {
@ -116,15 +113,7 @@ const Query: FC = () => {
table.values table.values
); );
const id = `${url}-executionError`; const id = `${url}-executionError`;
dispatch( showAlert('error', { id, title, message });
alertAdded({
id,
type: 'error',
title,
message,
createdAt: now(),
})
);
break; break;
} }
case 'Schema': { case 'Schema': {
@ -146,19 +135,7 @@ const Query: FC = () => {
} }
case 'Query Result': { case 'Query Result': {
const id = `${url}-querySuccess`; const id = `${url}-querySuccess`;
dispatch( showSuccessAlert({ id, title: 'Query succeed', message: '' });
alertAdded({
id,
type: 'success',
title: 'Query succeed',
message: '',
createdAt: now(),
})
);
setTimeout(() => {
dispatch(alertDissmissed(id));
}, AUTO_DISMISS_TIME);
break; break;
} }
case 'Source Description': case 'Source Description':
@ -175,20 +152,11 @@ const Query: FC = () => {
sse.onerror = () => { sse.onerror = () => {
// if it's open - we know that server responded without opening SSE // if it's open - we know that server responded without opening SSE
if (!sseRef.current.isOpen) { if (!sseRef.current.isOpen) {
const id = `${url}-connectionClosedError`; showAlert('error', {
dispatch( id: `${url}-connectionClosedError`,
alertAdded({ title: '',
id, message: 'SSE connection closed',
type: 'error', });
title: 'SSE connection closed',
message: '',
createdAt: now(),
})
);
setTimeout(() => {
dispatch(alertDissmissed(id));
}, AUTO_DISMISS_TIME);
} }
destroySSE(); destroySSE();
}; };

View file

@ -26,8 +26,7 @@ import {
selectAllSchemaVersions, selectAllSchemaVersions,
getSchemaLatest, getSchemaLatest,
} from 'redux/reducers/schemas/schemasSlice'; } from 'redux/reducers/schemas/schemasSlice';
import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice'; import { showServerError } from 'lib/errorHandling';
import { getResponse } from 'lib/errorHandling';
import { resetLoaderById } from 'redux/reducers/loader/loaderSlice'; import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
import { TableTitle } from 'components/common/table/TableTitle/TableTitle.styled'; import { TableTitle } from 'components/common/table/TableTitle/TableTitle.styled';
import useAppParams from 'lib/hooks/useAppParams'; import useAppParams from 'lib/hooks/useAppParams';
@ -73,8 +72,7 @@ const Details: React.FC = () => {
}); });
navigate('../'); navigate('../');
} catch (e) { } catch (e) {
const err = await getResponse(e as Response); showServerError(e as Response);
dispatch(serverErrorAlertAdded(err));
} }
}; };

View file

@ -22,11 +22,10 @@ import {
getAreSchemaLatestFulfilled, getAreSchemaLatestFulfilled,
schemaUpdated, schemaUpdated,
} from 'redux/reducers/schemas/schemasSlice'; } from 'redux/reducers/schemas/schemasSlice';
import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
import { getResponse } from 'lib/errorHandling';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
import { resetLoaderById } from 'redux/reducers/loader/loaderSlice'; import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
import { schemasApiClient } from 'lib/api'; import { schemasApiClient } from 'lib/api';
import { showServerError } from 'lib/errorHandling';
import * as S from './Edit.styled'; import * as S from './Edit.styled';
@ -91,8 +90,7 @@ const Edit: React.FC = () => {
navigate(clusterSchemaPath(clusterName, subject)); navigate(clusterSchemaPath(clusterName, subject));
} catch (e) { } catch (e) {
const err = await getResponse(e as Response); showServerError(e as Response);
dispatch(serverErrorAlertAdded(err));
} }
}; };

View file

@ -2,15 +2,14 @@ import React from 'react';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import Select from 'components/common/Select/Select'; import Select from 'components/common/Select/Select';
import { CompatibilityLevelCompatibilityEnum } from 'generated-sources'; import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
import { getResponse } from 'lib/errorHandling';
import { useAppDispatch } from 'lib/hooks/redux'; import { useAppDispatch } from 'lib/hooks/redux';
import usePagination from 'lib/hooks/usePagination'; import usePagination from 'lib/hooks/usePagination';
import useSearch from 'lib/hooks/useSearch'; import useSearch from 'lib/hooks/useSearch';
import useAppParams from 'lib/hooks/useAppParams'; import useAppParams from 'lib/hooks/useAppParams';
import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
import { fetchSchemas } from 'redux/reducers/schemas/schemasSlice'; import { fetchSchemas } from 'redux/reducers/schemas/schemasSlice';
import { ClusterNameRoute } from 'lib/paths'; import { ClusterNameRoute } from 'lib/paths';
import { schemasApiClient } from 'lib/api'; import { schemasApiClient } from 'lib/api';
import { showServerError } from 'lib/errorHandling';
import * as S from './GlobalSchemaSelector.styled'; import * as S from './GlobalSchemaSelector.styled';
@ -69,8 +68,7 @@ const GlobalSchemaSelector: React.FC = () => {
fetchSchemas({ clusterName, page, perPage, search: searchText }) fetchSchemas({ clusterName, page, perPage, search: searchText })
); );
} catch (e) { } catch (e) {
const err = await getResponse(e as Response); showServerError(e as Response);
dispatch(serverErrorAlertAdded(err));
} }
} }
setIsUpdating(false); setIsUpdating(false);

View file

@ -16,8 +16,7 @@ import PageHeading from 'components/common/PageHeading/PageHeading';
import { schemaAdded } from 'redux/reducers/schemas/schemasSlice'; import { schemaAdded } from 'redux/reducers/schemas/schemasSlice';
import { useAppDispatch } from 'lib/hooks/redux'; import { useAppDispatch } from 'lib/hooks/redux';
import useAppParams from 'lib/hooks/useAppParams'; import useAppParams from 'lib/hooks/useAppParams';
import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice'; import { showServerError } from 'lib/errorHandling';
import { getResponse } from 'lib/errorHandling';
import { schemasApiClient } from 'lib/api'; import { schemasApiClient } from 'lib/api';
import * as S from './New.styled'; import * as S from './New.styled';
@ -58,8 +57,7 @@ const New: React.FC = () => {
dispatch(schemaAdded(resp)); dispatch(schemaAdded(resp));
navigate(clusterSchemaPath(clusterName, subject)); navigate(clusterSchemaPath(clusterName, subject));
} catch (e) { } catch (e) {
const err = await getResponse(e as Response); showServerError(e as Response);
dispatch(serverErrorAlertAdded(err));
} }
}; };

View file

@ -11,8 +11,6 @@ import {
fetchTopicDetails, fetchTopicDetails,
} from 'redux/reducers/topics/topicsSlice'; } from 'redux/reducers/topics/topicsSlice';
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
import { alertAdded } from 'redux/reducers/alerts/alertsSlice';
import now from 'lodash/now';
import { Button } from 'components/common/Button/Button'; import { Button } from 'components/common/Button/Button';
import Editor from 'components/common/Editor/Editor'; import Editor from 'components/common/Editor/Editor';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
@ -25,7 +23,7 @@ import Select, { SelectOption } from 'components/common/Select/Select';
import useAppParams from 'lib/hooks/useAppParams'; import useAppParams from 'lib/hooks/useAppParams';
import Heading from 'components/common/heading/Heading.styled'; import Heading from 'components/common/heading/Heading.styled';
import { messagesApiClient } from 'lib/api'; import { messagesApiClient } from 'lib/api';
import { getResponse } from 'lib/errorHandling'; import { showAlert, showServerError } from 'lib/errorHandling';
import validateMessage from './validateMessage'; import validateMessage from './validateMessage';
import * as S from './SendMessage.styled'; import * as S from './SendMessage.styled';
@ -123,15 +121,11 @@ const SendMessage: React.FC = () => {
} }
if (errors.length > 0) { if (errors.length > 0) {
const errorsHtml = errors.map((e) => `<li>${e}</li>`).join(''); const errorsHtml = errors.map((e) => `<li>${e}</li>`).join('');
dispatch( showAlert('error', {
alertAdded({ id: `${clusterName}-${topicName}-createTopicMessageError`,
id: `${clusterName}-${topicName}-createTopicMessageError`, title: 'Validation Error',
type: 'error', message: `<ul>${errorsHtml}</ul>`,
title: 'Validation Error', });
message: `<ul>${errorsHtml}</ul>`,
createdAt: now(),
})
);
return; return;
} }
const headers = data.headers ? JSON.parse(data.headers) : undefined; const headers = data.headers ? JSON.parse(data.headers) : undefined;
@ -148,16 +142,10 @@ const SendMessage: React.FC = () => {
}); });
dispatch(fetchTopicDetails({ clusterName, topicName })); dispatch(fetchTopicDetails({ clusterName, topicName }));
} catch (e) { } catch (e) {
const err = await getResponse(e as Response); showServerError(e as Response, {
dispatch( id: `${clusterName}-${topicName}-sendTopicMessagesError`,
alertAdded({ message: `Error in sending a message to ${topicName}`,
id: `${clusterName}-${topicName}-sendTopicMessagesError`, });
type: 'error',
title: `Error in sending a message to ${topicName}`,
message: err?.message || '',
createdAt: now(),
})
);
} }
navigate(`../${clusterTopicMessagesRelativePath}`); navigate(`../${clusterTopicMessagesRelativePath}`);
} }

View file

@ -12,8 +12,7 @@ import { store } from 'redux/store';
import { fetchTopicDetails } from 'redux/reducers/topics/topicsSlice'; import { fetchTopicDetails } from 'redux/reducers/topics/topicsSlice';
import { externalTopicPayload } from 'redux/reducers/topics/__test__/fixtures'; import { externalTopicPayload } from 'redux/reducers/topics/__test__/fixtures';
import validateMessage from 'components/Topics/Topic/SendMessage/validateMessage'; import validateMessage from 'components/Topics/Topic/SendMessage/validateMessage';
import Alerts from 'components/Alerts/Alerts'; import { showServerError } from 'lib/errorHandling';
import * as S from 'components/App.styled';
import { testSchema } from './fixtures'; import { testSchema } from './fixtures';
@ -32,6 +31,11 @@ jest.mock('components/Topics/Topic/SendMessage/validateMessage', () =>
jest.fn() jest.fn()
); );
jest.mock('lib/errorHandling', () => ({
...jest.requireActual('lib/errorHandling'),
showServerError: jest.fn(),
}));
const mockNavigate = jest.fn(); const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
@ -44,14 +48,9 @@ const topicName = externalTopicPayload.name;
const renderComponent = async () => { const renderComponent = async () => {
await act(() => { await act(() => {
render( render(
<> <WithRoute path={clusterTopicSendMessagePath()}>
<WithRoute path={clusterTopicSendMessagePath()}> <SendMessage />
<SendMessage /> </WithRoute>,
</WithRoute>
<S.AlertsContainer role="toolbar">
<Alerts />
</S.AlertsContainer>
</>,
{ {
initialEntries: [clusterTopicSendMessagePath(clusterName, topicName)], initialEntries: [clusterTopicSendMessagePath(clusterName, topicName)],
store, store,
@ -101,9 +100,7 @@ describe('SendMessage', () => {
`/api/clusters/${clusterName}/topics/${topicName}/messages/schema`, `/api/clusters/${clusterName}/topics/${topicName}/messages/schema`,
testSchema testSchema
); );
await act(() => { await renderComponent();
renderComponent();
});
expect(fetchTopicMessageSchemaMock.called()).toBeTruthy(); expect(fetchTopicMessageSchemaMock.called()).toBeTruthy();
}); });
@ -121,7 +118,6 @@ describe('SendMessage', () => {
it('calls sendTopicMessage on submit', async () => { it('calls sendTopicMessage on submit', async () => {
const sendTopicMessageMock = fetchMock.postOnce(messagesUrl, 200); const sendTopicMessageMock = fetchMock.postOnce(messagesUrl, 200);
const fetchTopicDetailsMock = fetchMock.getOnce(detailsUrl, 200); const fetchTopicDetailsMock = fetchMock.getOnce(detailsUrl, 200);
await renderAndSubmitData(); await renderAndSubmitData();
expect(sendTopicMessageMock.called(messagesUrl)).toBeTruthy(); expect(sendTopicMessageMock.called(messagesUrl)).toBeTruthy();
expect(fetchTopicDetailsMock.called(detailsUrl)).toBeTruthy(); expect(fetchTopicDetailsMock.called(detailsUrl)).toBeTruthy();
@ -131,6 +127,8 @@ describe('SendMessage', () => {
}); });
it('should make the sendTopicMessage but most find an error within it', async () => { it('should make the sendTopicMessage but most find an error within it', async () => {
const showServerErrorMock = jest.fn();
(showServerError as jest.Mock).mockImplementation(showServerErrorMock);
const sendTopicMessageMock = fetchMock.postOnce(messagesUrl, { const sendTopicMessageMock = fetchMock.postOnce(messagesUrl, {
throws: 'Error', throws: 'Error',
}); });
@ -138,7 +136,12 @@ describe('SendMessage', () => {
await renderAndSubmitData(); await renderAndSubmitData();
expect(sendTopicMessageMock.called()).toBeTruthy(); expect(sendTopicMessageMock.called()).toBeTruthy();
expect(fetchTopicDetailsMock.called(detailsUrl)).toBeFalsy(); expect(fetchTopicDetailsMock.called(detailsUrl)).toBeFalsy();
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(showServerErrorMock).toHaveBeenCalledWith('Error', {
id: 'testCluster-external.topic-sendTopicMessagesError',
message: 'Error in sending a message to external.topic',
});
expect(mockNavigate).toHaveBeenLastCalledWith( expect(mockNavigate).toHaveBeenLastCalledWith(
`../${clusterTopicMessagesRelativePath}` `../${clusterTopicMessagesRelativePath}`
); );

View file

@ -1,7 +1,7 @@
import { AlertType } from 'redux/interfaces'; import { ToastType } from 'react-hot-toast';
import styled from 'styled-components'; import styled from 'styled-components';
export const Alert = styled.div<{ $type: AlertType }>` export const Alert = styled.div<{ $type: ToastType }>`
background-color: ${({ $type, theme }) => theme.alert.color[$type]}; background-color: ${({ $type, theme }) => theme.alert.color[$type]};
min-width: 400px; min-width: 400px;
min-height: 64px; min-height: 64px;

View file

@ -1,14 +1,14 @@
import React from 'react'; import React from 'react';
import CloseIcon from 'components/common/Icons/CloseIcon'; import CloseIcon from 'components/common/Icons/CloseIcon';
import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper'; import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
import { Alert as AlertType } from 'redux/interfaces'; import { ToastType } from 'react-hot-toast';
import * as S from './Alert.styled'; import * as S from './Alert.styled';
interface AlertProps { export interface AlertProps {
title: AlertType['title']; title: string;
type: AlertType['type']; type: ToastType;
message: AlertType['message']; message: string;
onDissmiss(): void; onDissmiss(): void;
} }
@ -21,7 +21,6 @@ const Alert: React.FC<AlertProps> = ({ title, type, message, onDissmiss }) => (
dangerouslySetInnerHTML={{ __html: message }} dangerouslySetInnerHTML={{ __html: message }}
/> />
</div> </div>
<IconButtonWrapper role="button" onClick={onDissmiss}> <IconButtonWrapper role="button" onClick={onDissmiss}>
<CloseIcon /> <CloseIcon />
</IconButtonWrapper> </IconButtonWrapper>

View file

@ -1,11 +1,9 @@
import React from 'react'; import React from 'react';
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { Alert as AlertProps } from 'redux/interfaces';
import Alert from 'components/Alerts/Alert';
import { render } from 'lib/testHelpers'; import { render } from 'lib/testHelpers';
import Alert, { AlertProps } from 'components/common/Alert/Alert';
const id = 'test-id';
const title = 'My Alert Title'; const title = 'My Alert Title';
const message = 'My Alert Message'; const message = 'My Alert Message';
const dismiss = jest.fn(); const dismiss = jest.fn();
@ -14,7 +12,6 @@ describe('Alert', () => {
const setupComponent = (props: Partial<AlertProps> = {}) => const setupComponent = (props: Partial<AlertProps> = {}) =>
render( render(
<Alert <Alert
id={id}
type="error" type="error"
title={title} title={title}
message={message} message={message}

View file

@ -1,5 +1,4 @@
import React, { PropsWithChildren } from 'react'; import React, { PropsWithChildren } from 'react';
import { AlertType } from 'redux/interfaces';
import * as S from './Metrics.styled'; import * as S from './Metrics.styled';
@ -8,7 +7,7 @@ export interface Props {
isAlert?: boolean; isAlert?: boolean;
label: React.ReactNode; label: React.ReactNode;
title?: string; title?: string;
alertType?: AlertType; alertType?: 'success' | 'error' | 'warning' | 'info';
} }
const Indicator: React.FC<PropsWithChildren<Props>> = ({ const Indicator: React.FC<PropsWithChildren<Props>> = ({

View file

@ -1,5 +1,4 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { AlertType } from 'redux/interfaces';
export const Wrapper = styled.div` export const Wrapper = styled.div`
padding: 1.5rem 1rem; padding: 1.5rem 1rem;
@ -75,7 +74,7 @@ export const CircularAlert = styled.circle.attrs({
cy: 2, cy: 2,
r: 2, r: 2,
})<{ })<{
$type: AlertType; $type: 'error' | 'success' | 'warning' | 'info';
}>( }>(
({ theme, $type }) => css` ({ theme, $type }) => css`
fill: ${theme.circularAlert.color[$type]}; fill: ${theme.circularAlert.color[$type]};

View file

@ -1,18 +0,0 @@
import { ServerResponse } from 'redux/interfaces';
export const getResponse = async (
response: Response
): Promise<ServerResponse> => {
let body;
try {
body = await response.json();
} catch (e) {
// do nothing;
}
return {
status: response.status,
statusText: response.statusText,
url: response.url,
message: body?.message,
};
};

View file

@ -0,0 +1,76 @@
import React from 'react';
import Alert from 'components/common/Alert/Alert';
import toast, { ToastType } from 'react-hot-toast';
import { ErrorResponse } from 'generated-sources';
interface ServerResponse {
status: number;
statusText: string;
url?: string;
message?: ErrorResponse['message'];
}
export const getResponse = async (
response: Response
): Promise<ServerResponse> => {
let body;
try {
body = await response.json();
} catch (e) {
// do nothing;
}
return {
status: response.status,
statusText: response.statusText,
url: response.url,
message: body?.message,
};
};
interface AlertOptions {
id?: string;
title?: string;
message: string;
}
export const showAlert = (
type: ToastType,
{ title, message, id }: AlertOptions
) => {
toast.custom(
(t) => (
<Alert
title={title || ''}
type={type}
message={message}
onDissmiss={() => toast.remove(t.id)}
/>
),
{ id }
);
};
export const showSuccessAlert = async (options: AlertOptions) => {
showAlert('success', {
...options,
title: options.title || 'Success',
});
};
export const showServerError = async (
response: Response,
options?: AlertOptions
) => {
let body: Record<string, string> = {};
try {
body = await response.json();
} catch (e) {
// do nothing;
}
showAlert('error', {
id: response.url,
title: `${response.status} ${response.statusText}`,
message: body?.message || 'An error occurred',
...options,
});
};

View file

@ -1,29 +1,20 @@
import isObject from 'lodash/isObject'; import isObject from 'lodash/isObject';
import { alertAdded, alertDissmissed } from 'redux/reducers/alerts/alertsSlice'; import { showSuccessAlert } from 'lib/errorHandling';
import { useAppDispatch } from 'lib/hooks/redux';
const AUTO_DISMISS_TIME = 2000;
const useDataSaver = ( const useDataSaver = (
subject: string, subject: string,
data: Record<string, string> | string data: Record<string, string> | string
) => { ) => {
const dispatch = useAppDispatch();
const copyToClipboard = () => { const copyToClipboard = () => {
if (navigator.clipboard) { if (navigator.clipboard) {
const str = const str =
typeof data === 'string' ? String(data) : JSON.stringify(data); typeof data === 'string' ? String(data) : JSON.stringify(data);
navigator.clipboard.writeText(str); navigator.clipboard.writeText(str);
dispatch( showSuccessAlert({
alertAdded({ id: subject,
id: subject, title: '',
type: 'success', message: 'Copied successfully!',
title: '', });
message: 'Copied successfully!',
createdAt: Date.now(),
})
);
setTimeout(() => dispatch(alertDissmissed(subject)), AUTO_DISMISS_TIME);
} }
}; };

View file

@ -1,19 +0,0 @@
import { ErrorResponse } from 'generated-sources';
export interface ServerResponse {
status: number;
statusText: string;
url?: string;
message?: ErrorResponse['message'];
}
export type AlertType = 'error' | 'success' | 'warning' | 'info';
export interface Alert {
id: string;
type: AlertType;
title: string;
message: string;
response?: ServerResponse;
createdAt: number;
}

View file

@ -6,7 +6,6 @@ export * from './cluster';
export * from './consumerGroup'; export * from './consumerGroup';
export * from './schema'; export * from './schema';
export * from './loader'; export * from './loader';
export * from './alerts';
export type RootState = ReturnType<typeof rootReducer>; export type RootState = ReturnType<typeof rootReducer>;
export type AppDispatch = typeof store.dispatch; export type AppDispatch = typeof store.dispatch;

View file

@ -1,100 +0,0 @@
import {
createAsyncThunk,
createEntityAdapter,
createSlice,
nanoid,
PayloadAction,
} from '@reduxjs/toolkit';
import { UnknownAsyncThunkRejectedWithValueAction } from '@reduxjs/toolkit/dist/matchers';
import now from 'lodash/now';
import { Alert, RootState, ServerResponse } from 'redux/interfaces';
const alertsAdapter = createEntityAdapter<Alert>({
selectId: (alert) => alert.id,
});
const isServerResponse = (payload: unknown): payload is ServerResponse => {
if ((payload as ServerResponse).status) {
return true;
}
return false;
};
const transformResponseToAlert = (payload: ServerResponse) => {
const { status, statusText, message, url } = payload;
const alert: Alert = {
id: url || nanoid(),
type: 'error',
title: `${status} ${statusText}`,
message: message || '',
response: payload,
createdAt: now(),
};
return alert;
};
const alertsSlice = createSlice({
name: 'alerts',
initialState: alertsAdapter.getInitialState(),
reducers: {
alertDissmissed: alertsAdapter.removeOne,
alertAdded(state, action: PayloadAction<Alert>) {
alertsAdapter.upsertOne(state, action.payload);
},
serverErrorAlertAdded: (
state,
{ payload }: PayloadAction<ServerResponse>
) => {
alertsAdapter.upsertOne(state, transformResponseToAlert(payload));
},
},
extraReducers: (builder) => {
builder.addMatcher(
(action): action is UnknownAsyncThunkRejectedWithValueAction =>
action.type.endsWith('/rejected'),
(state, { meta, payload }) => {
const { rejectedWithValue } = meta;
if (rejectedWithValue && isServerResponse(payload)) {
alertsAdapter.upsertOne(state, transformResponseToAlert(payload));
}
}
);
},
});
export const { selectAll } = alertsAdapter.getSelectors<RootState>(
(state) => state.alerts
);
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;

View file

@ -12,7 +12,11 @@ import {
SortOrder, SortOrder,
} from 'generated-sources'; } from 'generated-sources';
import { AsyncRequestStatus } from 'lib/constants'; import { AsyncRequestStatus } from 'lib/constants';
import { getResponse } from 'lib/errorHandling'; import {
getResponse,
showServerError,
showSuccessAlert,
} from 'lib/errorHandling';
import { import {
ClusterName, ClusterName,
ConsumerGroupID, ConsumerGroupID,
@ -49,6 +53,7 @@ export const fetchConsumerGroupsPaged = createAsyncThunk<
search, search,
}); });
} catch (error) { } catch (error) {
showServerError(error as Response);
return rejectWithValue(await getResponse(error as Response)); return rejectWithValue(await getResponse(error as Response));
} }
} }
@ -66,6 +71,7 @@ export const fetchConsumerGroupDetails = createAsyncThunk<
id: consumerGroupID, id: consumerGroupID,
}); });
} catch (error) { } catch (error) {
showServerError(error as Response);
return rejectWithValue(await getResponse(error as Response)); return rejectWithValue(await getResponse(error as Response));
} }
} }
@ -82,9 +88,12 @@ export const deleteConsumerGroup = createAsyncThunk<
clusterName, clusterName,
id: consumerGroupID, id: consumerGroupID,
}); });
showSuccessAlert({
message: `Consumer ${consumerGroupID} group deleted`,
});
return consumerGroupID; return consumerGroupID;
} catch (error) { } catch (error) {
showServerError(error as Response);
return rejectWithValue(await getResponse(error as Response)); return rejectWithValue(await getResponse(error as Response));
} }
} }
@ -114,8 +123,12 @@ export const resetConsumerGroupOffsets = createAsyncThunk<
resetToTimestamp: requestBody.resetToTimestamp?.getTime(), resetToTimestamp: requestBody.resetToTimestamp?.getTime(),
}, },
}); });
showSuccessAlert({
message: `Consumer ${consumerGroupID} group offsets reset`,
});
return consumerGroupID; return consumerGroupID;
} catch (error) { } catch (error) {
showServerError(error as Response);
return rejectWithValue(await getResponse(error as Response)); return rejectWithValue(await getResponse(error as Response));
} }
} }

View file

@ -1,6 +1,5 @@
import { combineReducers } from '@reduxjs/toolkit'; import { combineReducers } from '@reduxjs/toolkit';
import loader from 'redux/reducers/loader/loaderSlice'; import loader from 'redux/reducers/loader/loaderSlice';
import alerts from 'redux/reducers/alerts/alertsSlice';
import schemas from 'redux/reducers/schemas/schemasSlice'; import schemas from 'redux/reducers/schemas/schemasSlice';
import topicMessages from 'redux/reducers/topicMessages/topicMessagesSlice'; import topicMessages from 'redux/reducers/topicMessages/topicMessagesSlice';
import topics from 'redux/reducers/topics/topicsSlice'; import topics from 'redux/reducers/topics/topicsSlice';
@ -9,7 +8,6 @@ import ksqlDb from 'redux/reducers/ksqlDb/ksqlDbSlice';
export default combineReducers({ export default combineReducers({
loader, loader,
alerts,
topics, topics,
topicMessages, topicMessages,
consumerGroups, consumerGroups,

View file

@ -12,7 +12,7 @@ import {
} from 'generated-sources'; } from 'generated-sources';
import { schemasApiClient } from 'lib/api'; import { schemasApiClient } from 'lib/api';
import { AsyncRequestStatus } from 'lib/constants'; import { AsyncRequestStatus } from 'lib/constants';
import { getResponse } from 'lib/errorHandling'; import { getResponse, showServerError } from 'lib/errorHandling';
import { ClusterName, RootState } from 'redux/interfaces'; import { ClusterName, RootState } from 'redux/interfaces';
import { createFetchingSelector } from 'redux/reducers/loader/selectors'; import { createFetchingSelector } from 'redux/reducers/loader/selectors';
@ -24,6 +24,7 @@ export const fetchLatestSchema = createAsyncThunk<
try { try {
return await schemasApiClient.getLatestSchema(schemaParams); return await schemasApiClient.getLatestSchema(schemaParams);
} catch (error) { } catch (error) {
showServerError(error as Response);
return rejectWithValue(await getResponse(error as Response)); return rejectWithValue(await getResponse(error as Response));
} }
}); });
@ -43,6 +44,7 @@ export const fetchSchemas = createAsyncThunk<
search: search || undefined, search: search || undefined,
}); });
} catch (error) { } catch (error) {
showServerError(error as Response);
return rejectWithValue(await getResponse(error as Response)); return rejectWithValue(await getResponse(error as Response));
} }
} }
@ -61,6 +63,7 @@ export const fetchSchemaVersions = createAsyncThunk<
subject, subject,
}); });
} catch (error) { } catch (error) {
showServerError(error as Response);
return rejectWithValue(await getResponse(error as Response)); return rejectWithValue(await getResponse(error as Response));
} }
} }

View file

@ -1,8 +1,11 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { TopicMessagesState, ClusterName, TopicName } from 'redux/interfaces'; import { TopicMessagesState, ClusterName, TopicName } from 'redux/interfaces';
import { TopicMessage } from 'generated-sources'; import { TopicMessage } from 'generated-sources';
import { getResponse } from 'lib/errorHandling'; import {
import { showSuccessAlert } from 'redux/reducers/alerts/alertsSlice'; getResponse,
showServerError,
showSuccessAlert,
} from 'lib/errorHandling';
import { fetchTopicDetails } from 'redux/reducers/topics/topicsSlice'; import { fetchTopicDetails } from 'redux/reducers/topics/topicsSlice';
import { messagesApiClient } from 'lib/api'; import { messagesApiClient } from 'lib/api';
@ -22,15 +25,14 @@ export const clearTopicMessages = createAsyncThunk<
partitions, partitions,
}); });
dispatch(fetchTopicDetails({ clusterName, topicName })); dispatch(fetchTopicDetails({ clusterName, topicName }));
dispatch( showSuccessAlert({
showSuccessAlert({ id: `message-${topicName}-${clusterName}-${partitions}`,
id: `message-${topicName}-${clusterName}-${partitions}`, message: 'Messages successfully cleared!',
message: 'Messages successfully cleared!', });
})
);
return undefined; return undefined;
} catch (err) { } catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response)); return rejectWithValue(await getResponse(err as Response));
} }
} }

View file

@ -25,9 +25,12 @@ import {
TopicFormDataRaw, TopicFormDataRaw,
ClusterName, ClusterName,
} from 'redux/interfaces'; } from 'redux/interfaces';
import { getResponse } from 'lib/errorHandling'; import {
getResponse,
showServerError,
showSuccessAlert,
} from 'lib/errorHandling';
import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice'; import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
import { showSuccessAlert } from 'redux/reducers/alerts/alertsSlice';
import { import {
consumerGroupsApiClient, consumerGroupsApiClient,
messagesApiClient, messagesApiClient,
@ -41,6 +44,7 @@ export const fetchTopicsList = createAsyncThunk<
try { try {
return await topicsApiClient.getTopics(payload); return await topicsApiClient.getTopics(payload);
} catch (err) { } catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response)); return rejectWithValue(await getResponse(err as Response));
} }
}); });
@ -55,6 +59,7 @@ export const fetchTopicDetails = createAsyncThunk<
return { topicDetails, topicName }; return { topicDetails, topicName };
} catch (err) { } catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response)); return rejectWithValue(await getResponse(err as Response));
} }
}); });
@ -69,6 +74,7 @@ export const fetchTopicConfig = createAsyncThunk<
return { topicConfig, topicName }; return { topicConfig, topicName };
} catch (err) { } catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response)); return rejectWithValue(await getResponse(err as Response));
} }
}); });
@ -124,9 +130,12 @@ export const createTopic = createAsyncThunk<
clusterName, clusterName,
topicCreation: formatTopicCreation(data), topicCreation: formatTopicCreation(data),
}); });
showSuccessAlert({
message: `Topic ${data.name} created successfully`,
});
return undefined; return undefined;
} catch (err) { } catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response)); return rejectWithValue(await getResponse(err as Response));
} }
}); });
@ -143,6 +152,7 @@ export const fetchTopicConsumerGroups = createAsyncThunk<
return { consumerGroups, topicName }; return { consumerGroups, topicName };
} catch (err) { } catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response)); return rejectWithValue(await getResponse(err as Response));
} }
}); });
@ -188,6 +198,7 @@ export const updateTopic = createAsyncThunk<
return { topic }; return { topic };
} catch (err) { } catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response)); return rejectWithValue(await getResponse(err as Response));
} }
} }
@ -196,18 +207,17 @@ export const updateTopic = createAsyncThunk<
export const deleteTopic = createAsyncThunk< export const deleteTopic = createAsyncThunk<
{ topicName: TopicName }, { topicName: TopicName },
DeleteTopicRequest DeleteTopicRequest
>('topic/deleteTopic', async (payload, { rejectWithValue, dispatch }) => { >('topic/deleteTopic', async (payload, { rejectWithValue }) => {
try { try {
const { topicName, clusterName } = payload; const { topicName, clusterName } = payload;
await topicsApiClient.deleteTopic(payload); await topicsApiClient.deleteTopic(payload);
dispatch( showSuccessAlert({
showSuccessAlert({ id: `message-${topicName}-${clusterName}`,
id: `message-${topicName}-${clusterName}`, message: 'Topic successfully deleted!',
message: 'Topic successfully deleted!', });
})
);
return { topicName }; return { topicName };
} catch (err) { } catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response)); return rejectWithValue(await getResponse(err as Response));
} }
}); });
@ -215,19 +225,17 @@ export const deleteTopic = createAsyncThunk<
export const recreateTopic = createAsyncThunk< export const recreateTopic = createAsyncThunk<
{ topic: Topic }, { topic: Topic },
RecreateTopicRequest RecreateTopicRequest
>('topic/recreateTopic', async (payload, { rejectWithValue, dispatch }) => { >('topic/recreateTopic', async (payload, { rejectWithValue }) => {
try { try {
const { topicName, clusterName } = payload; const { topicName, clusterName } = payload;
const topic = await topicsApiClient.recreateTopic(payload); const topic = await topicsApiClient.recreateTopic(payload);
dispatch( showSuccessAlert({
showSuccessAlert({ id: `message-${topicName}-${clusterName}`,
id: `message-${topicName}-${clusterName}`, message: 'Topic successfully recreated!',
message: 'Topic successfully recreated!', });
})
);
return { topic }; return { topic };
} catch (err) { } catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response)); return rejectWithValue(await getResponse(err as Response));
} }
}); });
@ -241,6 +249,7 @@ export const fetchTopicMessageSchema = createAsyncThunk<
const schema = await messagesApiClient.getTopicSchema(payload); const schema = await messagesApiClient.getTopicSchema(payload);
return { schema, topicName }; return { schema, topicName };
} catch (err) { } catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response)); return rejectWithValue(await getResponse(err as Response));
} }
}); });
@ -263,15 +272,14 @@ export const updateTopicPartitionsCount = createAsyncThunk<
topicName, topicName,
partitionsIncrease: { totalPartitionsCount: partitions }, partitionsIncrease: { totalPartitionsCount: partitions },
}); });
dispatch( showSuccessAlert({
showSuccessAlert({ id: `message-${topicName}-${clusterName}-${partitions}`,
id: `message-${topicName}-${clusterName}-${partitions}`, message: 'Number of partitions successfully increased!',
message: 'Number of partitions successfully increased!', });
})
);
dispatch(fetchTopicDetails({ clusterName, topicName })); dispatch(fetchTopicDetails({ clusterName, topicName }));
return undefined; return undefined;
} catch (err) { } catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response)); return rejectWithValue(await getResponse(err as Response));
} }
} }
@ -298,6 +306,7 @@ export const updateTopicReplicationFactor = createAsyncThunk<
return undefined; return undefined;
} catch (err) { } catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response)); return rejectWithValue(await getResponse(err as Response));
} }
} }
@ -320,6 +329,7 @@ export const deleteTopics = createAsyncThunk<
return undefined; return undefined;
} catch (err) { } catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response)); return rejectWithValue(await getResponse(err as Response));
} }
}); });
@ -340,6 +350,7 @@ export const clearTopicsMessages = createAsyncThunk<
return undefined; return undefined;
} catch (err) { } catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response)); return rejectWithValue(await getResponse(err as Response));
} }
}); });

View file

@ -148,6 +148,9 @@ const theme = {
success: Colors.green[10], success: Colors.green[10],
warning: Colors.yellow[10], warning: Colors.yellow[10],
info: Colors.neutral[10], info: Colors.neutral[10],
loading: Colors.neutral[10],
blank: Colors.neutral[10],
custom: Colors.neutral[10],
}, },
shadow: Colors.transparency[20], shadow: Colors.transparency[20],
}, },