diff --git a/kafka-ui-react-app/package.json b/kafka-ui-react-app/package.json
index 9b3e523f6c..8dfb1fcd5d 100644
--- a/kafka-ui-react-app/package.json
+++ b/kafka-ui-react-app/package.json
@@ -34,6 +34,7 @@
"react-datepicker": "^4.8.0",
"react-dom": "^18.1.0",
"react-hook-form": "7.6.9",
+ "react-hot-toast": "^2.3.0",
"react-is": "^18.2.0",
"react-multi-select-component": "^4.0.6",
"react-redux": "^8.0.2",
diff --git a/kafka-ui-react-app/pnpm-lock.yaml b/kafka-ui-react-app/pnpm-lock.yaml
index d2128a2b5b..32fcce3a9c 100644
--- a/kafka-ui-react-app/pnpm-lock.yaml
+++ b/kafka-ui-react-app/pnpm-lock.yaml
@@ -70,6 +70,7 @@ specifiers:
react-datepicker: ^4.8.0
react-dom: ^18.1.0
react-hook-form: 7.6.9
+ react-hot-toast: ^2.3.0
react-is: ^18.2.0
react-multi-select-component: ^4.0.6
react-redux: ^8.0.2
@@ -119,6 +120,7 @@ dependencies:
react-datepicker: 4.8.0_ef5jwxihqo6n7gxfmzogljlgcm
react-dom: 18.1.0_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-multi-select-component: 4.0.6_react@18.1.0
react-redux: 8.0.2_nfqigfgwurfoimtkde74cji6ga
@@ -4957,6 +4959,12 @@ packages:
/globrex/0.1.2:
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:
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
@@ -6632,6 +6640,20 @@ packages:
react: 18.1.0
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:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
diff --git a/kafka-ui-react-app/src/components/Alerts/Alerts.tsx b/kafka-ui-react-app/src/components/Alerts/Alerts.tsx
deleted file mode 100644
index 11249dceae..0000000000
--- a/kafka-ui-react-app/src/components/Alerts/Alerts.tsx
+++ /dev/null
@@ -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 }) => (
-
- ))}
- >
- );
-};
-
-export default Alerts;
diff --git a/kafka-ui-react-app/src/components/Alerts/__tests__/Alerts.spec.tsx b/kafka-ui-react-app/src/components/Alerts/__tests__/Alerts.spec.tsx
deleted file mode 100644
index ee9fd0bacf..0000000000
--- a/kafka-ui-react-app/src/components/Alerts/__tests__/Alerts.spec.tsx
+++ /dev/null
@@ -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(, { 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);
- });
-});
diff --git a/kafka-ui-react-app/src/components/App.styled.ts b/kafka-ui-react-app/src/components/App.styled.ts
index cd79fe6249..71353f0876 100644
--- a/kafka-ui-react-app/src/components/App.styled.ts
+++ b/kafka-ui-react-app/src/components/App.styled.ts
@@ -44,7 +44,9 @@ export const Sidebar = styled.div<{ $visible: boolean }>(
background: ${theme.menu.backgroundColor.normal};
@media screen and (max-width: 1023px) {
${$visible &&
- `transform: translate3d(${theme.layout.navBarWidth}, 0, 0)`};
+ css`
+ transform: translate3d(${theme.layout.navBarWidth}, 0, 0);
+ `};
left: -${theme.layout.navBarWidth};
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)(
({ theme }) => css`
color: ${theme.button.primary.invertedColors.normal};
diff --git a/kafka-ui-react-app/src/components/App.tsx b/kafka-ui-react-app/src/components/App.tsx
index 0d23fc70b3..9de19134ea 100644
--- a/kafka-ui-react-app/src/components/App.tsx
+++ b/kafka-ui-react-app/src/components/App.tsx
@@ -7,10 +7,11 @@ import PageLoader from 'components/common/PageLoader/PageLoader';
import Dashboard from 'components/Dashboard/Dashboard';
import ClusterPage from 'components/Cluster/Cluster';
import Version from 'components/Version/Version';
-import Alerts from 'components/Alerts/Alerts';
import { ThemeProvider } from 'styled-components';
import theme from 'theme/theme';
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 Logo from './common/Logo/Logo';
@@ -22,6 +23,11 @@ const queryClient = new QueryClient({
queries: {
suspense: true,
},
+ mutations: {
+ onError(error) {
+ showServerError(error as Response);
+ },
+ },
},
});
@@ -113,9 +119,7 @@ const App: React.FC = () => {
/>
-
-
-
+
diff --git a/kafka-ui-react-app/src/components/KsqlDb/Query/Query.tsx b/kafka-ui-react-app/src/components/KsqlDb/Query/Query.tsx
index 6dfee76000..d7ddd6df17 100644
--- a/kafka-ui-react-app/src/components/KsqlDb/Query/Query.tsx
+++ b/kafka-ui-react-app/src/components/KsqlDb/Query/Query.tsx
@@ -8,17 +8,14 @@ import {
import { getKsqlExecution } from 'redux/reducers/ksqlDb/selectors';
import { BASE_PARAMS } from 'lib/constants';
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 { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
+import { showAlert, showSuccessAlert } from 'lib/errorHandling';
import type { FormValues } from './QueryForm/QueryForm';
import * as S from './Query.styled';
import QueryForm from './QueryForm/QueryForm';
-const AUTO_DISMISS_TIME = 8_000;
-
export const getFormattedErrorFromTableData = (
responseValues: KsqlTableResponse['values']
): { title: string; message: string } => {
@@ -116,15 +113,7 @@ const Query: FC = () => {
table.values
);
const id = `${url}-executionError`;
- dispatch(
- alertAdded({
- id,
- type: 'error',
- title,
- message,
- createdAt: now(),
- })
- );
+ showAlert('error', { id, title, message });
break;
}
case 'Schema': {
@@ -146,19 +135,7 @@ const Query: FC = () => {
}
case 'Query Result': {
const id = `${url}-querySuccess`;
- dispatch(
- alertAdded({
- id,
- type: 'success',
- title: 'Query succeed',
- message: '',
- createdAt: now(),
- })
- );
-
- setTimeout(() => {
- dispatch(alertDissmissed(id));
- }, AUTO_DISMISS_TIME);
+ showSuccessAlert({ id, title: 'Query succeed', message: '' });
break;
}
case 'Source Description':
@@ -175,20 +152,11 @@ const Query: FC = () => {
sse.onerror = () => {
// if it's open - we know that server responded without opening SSE
if (!sseRef.current.isOpen) {
- const id = `${url}-connectionClosedError`;
- dispatch(
- alertAdded({
- id,
- type: 'error',
- title: 'SSE connection closed',
- message: '',
- createdAt: now(),
- })
- );
-
- setTimeout(() => {
- dispatch(alertDissmissed(id));
- }, AUTO_DISMISS_TIME);
+ showAlert('error', {
+ id: `${url}-connectionClosedError`,
+ title: '',
+ message: 'SSE connection closed',
+ });
}
destroySSE();
};
diff --git a/kafka-ui-react-app/src/components/Schemas/Details/Details.tsx b/kafka-ui-react-app/src/components/Schemas/Details/Details.tsx
index 299b0984b1..9424409d76 100644
--- a/kafka-ui-react-app/src/components/Schemas/Details/Details.tsx
+++ b/kafka-ui-react-app/src/components/Schemas/Details/Details.tsx
@@ -26,8 +26,7 @@ import {
selectAllSchemaVersions,
getSchemaLatest,
} from 'redux/reducers/schemas/schemasSlice';
-import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
-import { getResponse } from 'lib/errorHandling';
+import { showServerError } from 'lib/errorHandling';
import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
import { TableTitle } from 'components/common/table/TableTitle/TableTitle.styled';
import useAppParams from 'lib/hooks/useAppParams';
@@ -73,8 +72,7 @@ const Details: React.FC = () => {
});
navigate('../');
} catch (e) {
- const err = await getResponse(e as Response);
- dispatch(serverErrorAlertAdded(err));
+ showServerError(e as Response);
}
};
diff --git a/kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx b/kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx
index 1b960675ec..7f4ea4491b 100644
--- a/kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx
+++ b/kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx
@@ -22,11 +22,10 @@ import {
getAreSchemaLatestFulfilled,
schemaUpdated,
} 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 { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
import { schemasApiClient } from 'lib/api';
+import { showServerError } from 'lib/errorHandling';
import * as S from './Edit.styled';
@@ -91,8 +90,7 @@ const Edit: React.FC = () => {
navigate(clusterSchemaPath(clusterName, subject));
} catch (e) {
- const err = await getResponse(e as Response);
- dispatch(serverErrorAlertAdded(err));
+ showServerError(e as Response);
}
};
diff --git a/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx b/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx
index dfe84c8f02..0bdd601a6f 100644
--- a/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx
+++ b/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx
@@ -2,15 +2,14 @@ import React from 'react';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import Select from 'components/common/Select/Select';
import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
-import { getResponse } from 'lib/errorHandling';
import { useAppDispatch } from 'lib/hooks/redux';
import usePagination from 'lib/hooks/usePagination';
import useSearch from 'lib/hooks/useSearch';
import useAppParams from 'lib/hooks/useAppParams';
-import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
import { fetchSchemas } from 'redux/reducers/schemas/schemasSlice';
import { ClusterNameRoute } from 'lib/paths';
import { schemasApiClient } from 'lib/api';
+import { showServerError } from 'lib/errorHandling';
import * as S from './GlobalSchemaSelector.styled';
@@ -69,8 +68,7 @@ const GlobalSchemaSelector: React.FC = () => {
fetchSchemas({ clusterName, page, perPage, search: searchText })
);
} catch (e) {
- const err = await getResponse(e as Response);
- dispatch(serverErrorAlertAdded(err));
+ showServerError(e as Response);
}
}
setIsUpdating(false);
diff --git a/kafka-ui-react-app/src/components/Schemas/New/New.tsx b/kafka-ui-react-app/src/components/Schemas/New/New.tsx
index e1474c1583..c3d9ba7003 100644
--- a/kafka-ui-react-app/src/components/Schemas/New/New.tsx
+++ b/kafka-ui-react-app/src/components/Schemas/New/New.tsx
@@ -16,8 +16,7 @@ import PageHeading from 'components/common/PageHeading/PageHeading';
import { schemaAdded } from 'redux/reducers/schemas/schemasSlice';
import { useAppDispatch } from 'lib/hooks/redux';
import useAppParams from 'lib/hooks/useAppParams';
-import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
-import { getResponse } from 'lib/errorHandling';
+import { showServerError } from 'lib/errorHandling';
import { schemasApiClient } from 'lib/api';
import * as S from './New.styled';
@@ -58,8 +57,7 @@ const New: React.FC = () => {
dispatch(schemaAdded(resp));
navigate(clusterSchemaPath(clusterName, subject));
} catch (e) {
- const err = await getResponse(e as Response);
- dispatch(serverErrorAlertAdded(err));
+ showServerError(e as Response);
}
};
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx
index 5889b7cb3b..3e7734a111 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx
+++ b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx
@@ -11,8 +11,6 @@ import {
fetchTopicDetails,
} from 'redux/reducers/topics/topicsSlice';
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 Editor from 'components/common/Editor/Editor';
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 Heading from 'components/common/heading/Heading.styled';
import { messagesApiClient } from 'lib/api';
-import { getResponse } from 'lib/errorHandling';
+import { showAlert, showServerError } from 'lib/errorHandling';
import validateMessage from './validateMessage';
import * as S from './SendMessage.styled';
@@ -123,15 +121,11 @@ const SendMessage: React.FC = () => {
}
if (errors.length > 0) {
const errorsHtml = errors.map((e) => `
${e}`).join('');
- dispatch(
- alertAdded({
- id: `${clusterName}-${topicName}-createTopicMessageError`,
- type: 'error',
- title: 'Validation Error',
- message: ``,
- createdAt: now(),
- })
- );
+ showAlert('error', {
+ id: `${clusterName}-${topicName}-createTopicMessageError`,
+ title: 'Validation Error',
+ message: ``,
+ });
return;
}
const headers = data.headers ? JSON.parse(data.headers) : undefined;
@@ -148,16 +142,10 @@ const SendMessage: React.FC = () => {
});
dispatch(fetchTopicDetails({ clusterName, topicName }));
} catch (e) {
- const err = await getResponse(e as Response);
- dispatch(
- alertAdded({
- id: `${clusterName}-${topicName}-sendTopicMessagesError`,
- type: 'error',
- title: `Error in sending a message to ${topicName}`,
- message: err?.message || '',
- createdAt: now(),
- })
- );
+ showServerError(e as Response, {
+ id: `${clusterName}-${topicName}-sendTopicMessagesError`,
+ message: `Error in sending a message to ${topicName}`,
+ });
}
navigate(`../${clusterTopicMessagesRelativePath}`);
}
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx
index ec37ec3320..76610d820a 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx
+++ b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx
@@ -12,8 +12,7 @@ import { store } from 'redux/store';
import { fetchTopicDetails } from 'redux/reducers/topics/topicsSlice';
import { externalTopicPayload } from 'redux/reducers/topics/__test__/fixtures';
import validateMessage from 'components/Topics/Topic/SendMessage/validateMessage';
-import Alerts from 'components/Alerts/Alerts';
-import * as S from 'components/App.styled';
+import { showServerError } from 'lib/errorHandling';
import { testSchema } from './fixtures';
@@ -32,6 +31,11 @@ jest.mock('components/Topics/Topic/SendMessage/validateMessage', () =>
jest.fn()
);
+jest.mock('lib/errorHandling', () => ({
+ ...jest.requireActual('lib/errorHandling'),
+ showServerError: jest.fn(),
+}));
+
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
@@ -44,14 +48,9 @@ const topicName = externalTopicPayload.name;
const renderComponent = async () => {
await act(() => {
render(
- <>
-
-
-
-
-
-
- >,
+
+
+ ,
{
initialEntries: [clusterTopicSendMessagePath(clusterName, topicName)],
store,
@@ -101,9 +100,7 @@ describe('SendMessage', () => {
`/api/clusters/${clusterName}/topics/${topicName}/messages/schema`,
testSchema
);
- await act(() => {
- renderComponent();
- });
+ await renderComponent();
expect(fetchTopicMessageSchemaMock.called()).toBeTruthy();
});
@@ -121,7 +118,6 @@ describe('SendMessage', () => {
it('calls sendTopicMessage on submit', async () => {
const sendTopicMessageMock = fetchMock.postOnce(messagesUrl, 200);
const fetchTopicDetailsMock = fetchMock.getOnce(detailsUrl, 200);
-
await renderAndSubmitData();
expect(sendTopicMessageMock.called(messagesUrl)).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 () => {
+ const showServerErrorMock = jest.fn();
+ (showServerError as jest.Mock).mockImplementation(showServerErrorMock);
const sendTopicMessageMock = fetchMock.postOnce(messagesUrl, {
throws: 'Error',
});
@@ -138,7 +136,12 @@ describe('SendMessage', () => {
await renderAndSubmitData();
expect(sendTopicMessageMock.called()).toBeTruthy();
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(
`../${clusterTopicMessagesRelativePath}`
);
diff --git a/kafka-ui-react-app/src/components/Alerts/Alert.styled.ts b/kafka-ui-react-app/src/components/common/Alert/Alert.styled.ts
similarity index 86%
rename from kafka-ui-react-app/src/components/Alerts/Alert.styled.ts
rename to kafka-ui-react-app/src/components/common/Alert/Alert.styled.ts
index 298f874903..179db51ead 100644
--- a/kafka-ui-react-app/src/components/Alerts/Alert.styled.ts
+++ b/kafka-ui-react-app/src/components/common/Alert/Alert.styled.ts
@@ -1,7 +1,7 @@
-import { AlertType } from 'redux/interfaces';
+import { ToastType } from 'react-hot-toast';
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]};
min-width: 400px;
min-height: 64px;
diff --git a/kafka-ui-react-app/src/components/Alerts/Alert.tsx b/kafka-ui-react-app/src/components/common/Alert/Alert.tsx
similarity index 79%
rename from kafka-ui-react-app/src/components/Alerts/Alert.tsx
rename to kafka-ui-react-app/src/components/common/Alert/Alert.tsx
index af564b2396..6a82e6f13c 100644
--- a/kafka-ui-react-app/src/components/Alerts/Alert.tsx
+++ b/kafka-ui-react-app/src/components/common/Alert/Alert.tsx
@@ -1,14 +1,14 @@
import React from 'react';
import CloseIcon from 'components/common/Icons/CloseIcon';
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';
-interface AlertProps {
- title: AlertType['title'];
- type: AlertType['type'];
- message: AlertType['message'];
+export interface AlertProps {
+ title: string;
+ type: ToastType;
+ message: string;
onDissmiss(): void;
}
@@ -21,7 +21,6 @@ const Alert: React.FC = ({ title, type, message, onDissmiss }) => (
dangerouslySetInnerHTML={{ __html: message }}
/>
-
diff --git a/kafka-ui-react-app/src/components/Alerts/__tests__/Alert.spec.tsx b/kafka-ui-react-app/src/components/common/Alert/__tests__/Alert.spec.tsx
similarity index 87%
rename from kafka-ui-react-app/src/components/Alerts/__tests__/Alert.spec.tsx
rename to kafka-ui-react-app/src/components/common/Alert/__tests__/Alert.spec.tsx
index 382eff6fe9..a77f451b5e 100644
--- a/kafka-ui-react-app/src/components/Alerts/__tests__/Alert.spec.tsx
+++ b/kafka-ui-react-app/src/components/common/Alert/__tests__/Alert.spec.tsx
@@ -1,11 +1,9 @@
import React from 'react';
import { screen } from '@testing-library/react';
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 Alert, { AlertProps } from 'components/common/Alert/Alert';
-const id = 'test-id';
const title = 'My Alert Title';
const message = 'My Alert Message';
const dismiss = jest.fn();
@@ -14,7 +12,6 @@ describe('Alert', () => {
const setupComponent = (props: Partial = {}) =>
render(
> = ({
diff --git a/kafka-ui-react-app/src/components/common/Metrics/Metrics.styled.tsx b/kafka-ui-react-app/src/components/common/Metrics/Metrics.styled.tsx
index 9924c5ba13..2d66121891 100644
--- a/kafka-ui-react-app/src/components/common/Metrics/Metrics.styled.tsx
+++ b/kafka-ui-react-app/src/components/common/Metrics/Metrics.styled.tsx
@@ -1,5 +1,4 @@
import styled, { css } from 'styled-components';
-import { AlertType } from 'redux/interfaces';
export const Wrapper = styled.div`
padding: 1.5rem 1rem;
@@ -75,7 +74,7 @@ export const CircularAlert = styled.circle.attrs({
cy: 2,
r: 2,
})<{
- $type: AlertType;
+ $type: 'error' | 'success' | 'warning' | 'info';
}>(
({ theme, $type }) => css`
fill: ${theme.circularAlert.color[$type]};
diff --git a/kafka-ui-react-app/src/lib/errorHandling.ts b/kafka-ui-react-app/src/lib/errorHandling.ts
deleted file mode 100644
index 1240a666c7..0000000000
--- a/kafka-ui-react-app/src/lib/errorHandling.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { ServerResponse } from 'redux/interfaces';
-
-export const getResponse = async (
- response: Response
-): Promise => {
- let body;
- try {
- body = await response.json();
- } catch (e) {
- // do nothing;
- }
- return {
- status: response.status,
- statusText: response.statusText,
- url: response.url,
- message: body?.message,
- };
-};
diff --git a/kafka-ui-react-app/src/lib/errorHandling.tsx b/kafka-ui-react-app/src/lib/errorHandling.tsx
new file mode 100644
index 0000000000..b839cfff35
--- /dev/null
+++ b/kafka-ui-react-app/src/lib/errorHandling.tsx
@@ -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 => {
+ 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) => (
+ 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 = {};
+ 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,
+ });
+};
diff --git a/kafka-ui-react-app/src/lib/hooks/useDataSaver.ts b/kafka-ui-react-app/src/lib/hooks/useDataSaver.ts
index 9170865958..20c9cd2ac2 100644
--- a/kafka-ui-react-app/src/lib/hooks/useDataSaver.ts
+++ b/kafka-ui-react-app/src/lib/hooks/useDataSaver.ts
@@ -1,29 +1,20 @@
import isObject from 'lodash/isObject';
-import { alertAdded, alertDissmissed } from 'redux/reducers/alerts/alertsSlice';
-import { useAppDispatch } from 'lib/hooks/redux';
-
-const AUTO_DISMISS_TIME = 2000;
+import { showSuccessAlert } from 'lib/errorHandling';
const useDataSaver = (
subject: string,
data: Record | string
) => {
- const dispatch = useAppDispatch();
const copyToClipboard = () => {
if (navigator.clipboard) {
const str =
typeof data === 'string' ? String(data) : JSON.stringify(data);
navigator.clipboard.writeText(str);
- dispatch(
- alertAdded({
- id: subject,
- type: 'success',
- title: '',
- message: 'Copied successfully!',
- createdAt: Date.now(),
- })
- );
- setTimeout(() => dispatch(alertDissmissed(subject)), AUTO_DISMISS_TIME);
+ showSuccessAlert({
+ id: subject,
+ title: '',
+ message: 'Copied successfully!',
+ });
}
};
diff --git a/kafka-ui-react-app/src/redux/interfaces/alerts.ts b/kafka-ui-react-app/src/redux/interfaces/alerts.ts
deleted file mode 100644
index 7473ec7f00..0000000000
--- a/kafka-ui-react-app/src/redux/interfaces/alerts.ts
+++ /dev/null
@@ -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;
-}
diff --git a/kafka-ui-react-app/src/redux/interfaces/index.ts b/kafka-ui-react-app/src/redux/interfaces/index.ts
index c77496e848..0c8ddf234e 100644
--- a/kafka-ui-react-app/src/redux/interfaces/index.ts
+++ b/kafka-ui-react-app/src/redux/interfaces/index.ts
@@ -6,7 +6,6 @@ export * from './cluster';
export * from './consumerGroup';
export * from './schema';
export * from './loader';
-export * from './alerts';
export type RootState = ReturnType;
export type AppDispatch = typeof store.dispatch;
diff --git a/kafka-ui-react-app/src/redux/reducers/alerts/alertsSlice.ts b/kafka-ui-react-app/src/redux/reducers/alerts/alertsSlice.ts
deleted file mode 100644
index 1890ceb025..0000000000
--- a/kafka-ui-react-app/src/redux/reducers/alerts/alertsSlice.ts
+++ /dev/null
@@ -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({
- 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) {
- alertsAdapter.upsertOne(state, action.payload);
- },
- serverErrorAlertAdded: (
- state,
- { payload }: PayloadAction
- ) => {
- 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(
- (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;
diff --git a/kafka-ui-react-app/src/redux/reducers/consumerGroups/consumerGroupsSlice.ts b/kafka-ui-react-app/src/redux/reducers/consumerGroups/consumerGroupsSlice.ts
index 063ca58609..2b7709b009 100644
--- a/kafka-ui-react-app/src/redux/reducers/consumerGroups/consumerGroupsSlice.ts
+++ b/kafka-ui-react-app/src/redux/reducers/consumerGroups/consumerGroupsSlice.ts
@@ -12,7 +12,11 @@ import {
SortOrder,
} from 'generated-sources';
import { AsyncRequestStatus } from 'lib/constants';
-import { getResponse } from 'lib/errorHandling';
+import {
+ getResponse,
+ showServerError,
+ showSuccessAlert,
+} from 'lib/errorHandling';
import {
ClusterName,
ConsumerGroupID,
@@ -49,6 +53,7 @@ export const fetchConsumerGroupsPaged = createAsyncThunk<
search,
});
} catch (error) {
+ showServerError(error as Response);
return rejectWithValue(await getResponse(error as Response));
}
}
@@ -66,6 +71,7 @@ export const fetchConsumerGroupDetails = createAsyncThunk<
id: consumerGroupID,
});
} catch (error) {
+ showServerError(error as Response);
return rejectWithValue(await getResponse(error as Response));
}
}
@@ -82,9 +88,12 @@ export const deleteConsumerGroup = createAsyncThunk<
clusterName,
id: consumerGroupID,
});
-
+ showSuccessAlert({
+ message: `Consumer ${consumerGroupID} group deleted`,
+ });
return consumerGroupID;
} catch (error) {
+ showServerError(error as Response);
return rejectWithValue(await getResponse(error as Response));
}
}
@@ -114,8 +123,12 @@ export const resetConsumerGroupOffsets = createAsyncThunk<
resetToTimestamp: requestBody.resetToTimestamp?.getTime(),
},
});
+ showSuccessAlert({
+ message: `Consumer ${consumerGroupID} group offsets reset`,
+ });
return consumerGroupID;
} catch (error) {
+ showServerError(error as Response);
return rejectWithValue(await getResponse(error as Response));
}
}
diff --git a/kafka-ui-react-app/src/redux/reducers/index.ts b/kafka-ui-react-app/src/redux/reducers/index.ts
index ee5421f89e..92b9246318 100644
--- a/kafka-ui-react-app/src/redux/reducers/index.ts
+++ b/kafka-ui-react-app/src/redux/reducers/index.ts
@@ -1,6 +1,5 @@
import { combineReducers } from '@reduxjs/toolkit';
import loader from 'redux/reducers/loader/loaderSlice';
-import alerts from 'redux/reducers/alerts/alertsSlice';
import schemas from 'redux/reducers/schemas/schemasSlice';
import topicMessages from 'redux/reducers/topicMessages/topicMessagesSlice';
import topics from 'redux/reducers/topics/topicsSlice';
@@ -9,7 +8,6 @@ import ksqlDb from 'redux/reducers/ksqlDb/ksqlDbSlice';
export default combineReducers({
loader,
- alerts,
topics,
topicMessages,
consumerGroups,
diff --git a/kafka-ui-react-app/src/redux/reducers/schemas/schemasSlice.ts b/kafka-ui-react-app/src/redux/reducers/schemas/schemasSlice.ts
index 7d52b78741..049e0bedd7 100644
--- a/kafka-ui-react-app/src/redux/reducers/schemas/schemasSlice.ts
+++ b/kafka-ui-react-app/src/redux/reducers/schemas/schemasSlice.ts
@@ -12,7 +12,7 @@ import {
} from 'generated-sources';
import { schemasApiClient } from 'lib/api';
import { AsyncRequestStatus } from 'lib/constants';
-import { getResponse } from 'lib/errorHandling';
+import { getResponse, showServerError } from 'lib/errorHandling';
import { ClusterName, RootState } from 'redux/interfaces';
import { createFetchingSelector } from 'redux/reducers/loader/selectors';
@@ -24,6 +24,7 @@ export const fetchLatestSchema = createAsyncThunk<
try {
return await schemasApiClient.getLatestSchema(schemaParams);
} catch (error) {
+ showServerError(error as Response);
return rejectWithValue(await getResponse(error as Response));
}
});
@@ -43,6 +44,7 @@ export const fetchSchemas = createAsyncThunk<
search: search || undefined,
});
} catch (error) {
+ showServerError(error as Response);
return rejectWithValue(await getResponse(error as Response));
}
}
@@ -61,6 +63,7 @@ export const fetchSchemaVersions = createAsyncThunk<
subject,
});
} catch (error) {
+ showServerError(error as Response);
return rejectWithValue(await getResponse(error as Response));
}
}
diff --git a/kafka-ui-react-app/src/redux/reducers/topicMessages/topicMessagesSlice.ts b/kafka-ui-react-app/src/redux/reducers/topicMessages/topicMessagesSlice.ts
index 375c99e533..49e12d453e 100644
--- a/kafka-ui-react-app/src/redux/reducers/topicMessages/topicMessagesSlice.ts
+++ b/kafka-ui-react-app/src/redux/reducers/topicMessages/topicMessagesSlice.ts
@@ -1,8 +1,11 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { TopicMessagesState, ClusterName, TopicName } from 'redux/interfaces';
import { TopicMessage } from 'generated-sources';
-import { getResponse } from 'lib/errorHandling';
-import { showSuccessAlert } from 'redux/reducers/alerts/alertsSlice';
+import {
+ getResponse,
+ showServerError,
+ showSuccessAlert,
+} from 'lib/errorHandling';
import { fetchTopicDetails } from 'redux/reducers/topics/topicsSlice';
import { messagesApiClient } from 'lib/api';
@@ -22,15 +25,14 @@ export const clearTopicMessages = createAsyncThunk<
partitions,
});
dispatch(fetchTopicDetails({ clusterName, topicName }));
- dispatch(
- showSuccessAlert({
- id: `message-${topicName}-${clusterName}-${partitions}`,
- message: 'Messages successfully cleared!',
- })
- );
+ showSuccessAlert({
+ id: `message-${topicName}-${clusterName}-${partitions}`,
+ message: 'Messages successfully cleared!',
+ });
return undefined;
} catch (err) {
+ showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
}
}
diff --git a/kafka-ui-react-app/src/redux/reducers/topics/topicsSlice.ts b/kafka-ui-react-app/src/redux/reducers/topics/topicsSlice.ts
index 629d747110..271cc7798a 100644
--- a/kafka-ui-react-app/src/redux/reducers/topics/topicsSlice.ts
+++ b/kafka-ui-react-app/src/redux/reducers/topics/topicsSlice.ts
@@ -25,9 +25,12 @@ import {
TopicFormDataRaw,
ClusterName,
} from 'redux/interfaces';
-import { getResponse } from 'lib/errorHandling';
+import {
+ getResponse,
+ showServerError,
+ showSuccessAlert,
+} from 'lib/errorHandling';
import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
-import { showSuccessAlert } from 'redux/reducers/alerts/alertsSlice';
import {
consumerGroupsApiClient,
messagesApiClient,
@@ -41,6 +44,7 @@ export const fetchTopicsList = createAsyncThunk<
try {
return await topicsApiClient.getTopics(payload);
} catch (err) {
+ showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
}
});
@@ -55,6 +59,7 @@ export const fetchTopicDetails = createAsyncThunk<
return { topicDetails, topicName };
} catch (err) {
+ showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
}
});
@@ -69,6 +74,7 @@ export const fetchTopicConfig = createAsyncThunk<
return { topicConfig, topicName };
} catch (err) {
+ showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
}
});
@@ -124,9 +130,12 @@ export const createTopic = createAsyncThunk<
clusterName,
topicCreation: formatTopicCreation(data),
});
-
+ showSuccessAlert({
+ message: `Topic ${data.name} created successfully`,
+ });
return undefined;
} catch (err) {
+ showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
}
});
@@ -143,6 +152,7 @@ export const fetchTopicConsumerGroups = createAsyncThunk<
return { consumerGroups, topicName };
} catch (err) {
+ showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
}
});
@@ -188,6 +198,7 @@ export const updateTopic = createAsyncThunk<
return { topic };
} catch (err) {
+ showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
}
}
@@ -196,18 +207,17 @@ export const updateTopic = createAsyncThunk<
export const deleteTopic = createAsyncThunk<
{ topicName: TopicName },
DeleteTopicRequest
->('topic/deleteTopic', async (payload, { rejectWithValue, dispatch }) => {
+>('topic/deleteTopic', async (payload, { rejectWithValue }) => {
try {
const { topicName, clusterName } = payload;
await topicsApiClient.deleteTopic(payload);
- dispatch(
- showSuccessAlert({
- id: `message-${topicName}-${clusterName}`,
- message: 'Topic successfully deleted!',
- })
- );
+ showSuccessAlert({
+ id: `message-${topicName}-${clusterName}`,
+ message: 'Topic successfully deleted!',
+ });
return { topicName };
} catch (err) {
+ showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
}
});
@@ -215,19 +225,17 @@ export const deleteTopic = createAsyncThunk<
export const recreateTopic = createAsyncThunk<
{ topic: Topic },
RecreateTopicRequest
->('topic/recreateTopic', async (payload, { rejectWithValue, dispatch }) => {
+>('topic/recreateTopic', async (payload, { rejectWithValue }) => {
try {
const { topicName, clusterName } = payload;
const topic = await topicsApiClient.recreateTopic(payload);
- dispatch(
- showSuccessAlert({
- id: `message-${topicName}-${clusterName}`,
- message: 'Topic successfully recreated!',
- })
- );
-
+ showSuccessAlert({
+ id: `message-${topicName}-${clusterName}`,
+ message: 'Topic successfully recreated!',
+ });
return { topic };
} catch (err) {
+ showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
}
});
@@ -241,6 +249,7 @@ export const fetchTopicMessageSchema = createAsyncThunk<
const schema = await messagesApiClient.getTopicSchema(payload);
return { schema, topicName };
} catch (err) {
+ showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
}
});
@@ -263,15 +272,14 @@ export const updateTopicPartitionsCount = createAsyncThunk<
topicName,
partitionsIncrease: { totalPartitionsCount: partitions },
});
- dispatch(
- showSuccessAlert({
- id: `message-${topicName}-${clusterName}-${partitions}`,
- message: 'Number of partitions successfully increased!',
- })
- );
+ showSuccessAlert({
+ id: `message-${topicName}-${clusterName}-${partitions}`,
+ message: 'Number of partitions successfully increased!',
+ });
dispatch(fetchTopicDetails({ clusterName, topicName }));
return undefined;
} catch (err) {
+ showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
}
}
@@ -298,6 +306,7 @@ export const updateTopicReplicationFactor = createAsyncThunk<
return undefined;
} catch (err) {
+ showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
}
}
@@ -320,6 +329,7 @@ export const deleteTopics = createAsyncThunk<
return undefined;
} catch (err) {
+ showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
}
});
@@ -340,6 +350,7 @@ export const clearTopicsMessages = createAsyncThunk<
return undefined;
} catch (err) {
+ showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
}
});
diff --git a/kafka-ui-react-app/src/theme/theme.ts b/kafka-ui-react-app/src/theme/theme.ts
index c5d54a8b59..bf79a8148f 100644
--- a/kafka-ui-react-app/src/theme/theme.ts
+++ b/kafka-ui-react-app/src/theme/theme.ts
@@ -148,6 +148,9 @@ const theme = {
success: Colors.green[10],
warning: Colors.yellow[10],
info: Colors.neutral[10],
+ loading: Colors.neutral[10],
+ blank: Colors.neutral[10],
+ custom: Colors.neutral[10],
},
shadow: Colors.transparency[20],
},