Use react-hot-toaster for alerts (#2312)
* Use react-hot-toaster for alerts * Fix linting problems
This commit is contained in:
parent
48325bc5ad
commit
bffe316063
30 changed files with 237 additions and 394 deletions
|
@ -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",
|
||||||
|
|
22
kafka-ui-react-app/pnpm-lock.yaml
generated
22
kafka-ui-react-app/pnpm-lock.yaml
generated
|
@ -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==}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
};
|
};
|
||||||
|
|
|
@ -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));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
|
@ -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>
|
|
@ -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}
|
|
@ -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>> = ({
|
||||||
|
|
|
@ -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]};
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
76
kafka-ui-react-app/src/lib/errorHandling.tsx
Normal file
76
kafka-ui-react-app/src/lib/errorHandling.tsx
Normal 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,
|
||||||
|
});
|
||||||
|
};
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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],
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Reference in a new issue