diff --git a/openmetadata-ui-core-components/src/main/resources/ui/package.json b/openmetadata-ui-core-components/src/main/resources/ui/package.json
index 32b6451e48e..03ac4b60a5a 100644
--- a/openmetadata-ui-core-components/src/main/resources/ui/package.json
+++ b/openmetadata-ui-core-components/src/main/resources/ui/package.json
@@ -70,7 +70,8 @@
"react": ">=18.0.0",
"react-dom": ">=18.0.0",
"@emotion/react": ">=11.0.0",
- "@emotion/styled": ">=11.0.0"
+ "@emotion/styled": ">=11.0.0",
+ "notistack": ">=3.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
@@ -87,7 +88,8 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@emotion/react": "^11.14.0",
- "@emotion/styled": "^11.14.1"
+ "@emotion/styled": "^11.14.1",
+ "notistack": "^3.0.1"
},
"publishConfig": {
"access": "restricted",
diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/SnackbarContent.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/SnackbarContent.tsx
new file mode 100644
index 00000000000..6a6e05e989a
--- /dev/null
+++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/SnackbarContent.tsx
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2025 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { MaterialDesignContent } from 'notistack';
+import { styled } from '@mui/material/styles';
+
+export const SnackbarContent = styled(MaterialDesignContent)(({ theme }) => ({
+ // Base styles matching MuiAlert from data-display-theme.ts
+ '&.notistack-MuiContent': {
+ backgroundColor: theme.palette.background.paper || '#FFFFFF',
+ border: `1px solid ${theme.palette.grey?.[300] || '#D2D4DB'}`,
+ color: theme.palette.text.primary || '#181D27',
+ borderRadius: '12px',
+ boxShadow: '0px 1px 2px rgba(10, 13, 18, 0.05)',
+ fontSize: '0.875rem',
+ padding: '12px 16px',
+ fontFamily: 'var(--font-inter, "Inter"), -apple-system, "Segoe UI", Roboto, Arial, sans-serif',
+
+ // Override default notistack styles
+ '& .notistack-MuiContent-message': {
+ padding: 0,
+ fontWeight: 400,
+ lineHeight: '1.25rem',
+ },
+ },
+
+ // Variant-specific border colors
+ '&.notistack-MuiContent-error': {
+ backgroundColor: theme.palette.background.paper || '#FFFFFF',
+ borderColor: theme.palette.error.light || '#F79E9E',
+ color: theme.palette.text.primary || '#181D27',
+ },
+ '&.notistack-MuiContent-success': {
+ backgroundColor: theme.palette.background.paper || '#FFFFFF',
+ borderColor: theme.palette.success.light || '#83D2A3',
+ color: theme.palette.text.primary || '#181D27',
+ },
+ '&.notistack-MuiContent-warning': {
+ backgroundColor: theme.palette.background.paper || '#FFFFFF',
+ borderColor: theme.palette.warning.light || '#FFBE7F',
+ color: theme.palette.text.primary || '#181D27',
+ },
+ '&.notistack-MuiContent-info': {
+ backgroundColor: theme.palette.background.paper || '#FFFFFF',
+ borderColor: theme.palette.info.light || '#7BAEFF',
+ color: theme.palette.text.primary || '#181D27',
+ },
+ '&.notistack-MuiContent-default': {
+ backgroundColor: theme.palette.background.paper || '#FFFFFF',
+ borderColor: theme.palette.grey?.[300] || '#D2D4DB',
+ color: theme.palette.text.primary || '#181D27',
+ },
+}));
\ No newline at end of file
diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/index.ts b/openmetadata-ui-core-components/src/main/resources/ui/src/components/index.ts
index 1c4f609ceff..e35fe3b764e 100644
--- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/index.ts
+++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/index.ts
@@ -1,2 +1,3 @@
// Component exports
-export * from './checkbox-icons';
\ No newline at end of file
+export * from './checkbox-icons';
+export { SnackbarContent } from './SnackbarContent';
\ No newline at end of file
diff --git a/openmetadata-ui-core-components/src/main/resources/ui/vite.config.ts b/openmetadata-ui-core-components/src/main/resources/ui/vite.config.ts
index 5403bd2935d..aac9afd01d7 100644
--- a/openmetadata-ui-core-components/src/main/resources/ui/vite.config.ts
+++ b/openmetadata-ui-core-components/src/main/resources/ui/vite.config.ts
@@ -21,8 +21,8 @@ export default defineConfig({
},
rollupOptions: {
external: [
- 'react',
- 'react-dom',
+ 'react',
+ 'react-dom',
'react/jsx-runtime',
'@mui/material',
'@mui/system',
@@ -32,7 +32,8 @@ export default defineConfig({
'@mui/x-date-pickers',
'@emotion/react',
'@emotion/styled',
- '@material/material-color-utilities'
+ '@material/material-color-utilities',
+ 'notistack'
],
output: {
globals: {
@@ -41,7 +42,8 @@ export default defineConfig({
'@mui/material': 'MaterialUI',
'@mui/system': 'MUISystem',
'@emotion/react': 'EmotionReact',
- '@emotion/styled': 'EmotionStyled'
+ '@emotion/styled': 'EmotionStyled',
+ 'notistack': 'notistack'
}
}
},
diff --git a/openmetadata-ui-core-components/src/main/resources/ui/yarn.lock b/openmetadata-ui-core-components/src/main/resources/ui/yarn.lock
index a53cc5a3f0c..4d1c31db8dd 100644
--- a/openmetadata-ui-core-components/src/main/resources/ui/yarn.lock
+++ b/openmetadata-ui-core-components/src/main/resources/ui/yarn.lock
@@ -966,6 +966,11 @@ caniuse-lite@^1.0.30001737:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz#b34ce2d56bfc22f4352b2af0144102d623a124f4"
integrity sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==
+clsx@^1.1.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
+ integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
+
clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
@@ -1137,6 +1142,11 @@ gensync@^1.0.0-beta.2:
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
+goober@^2.0.33:
+ version "2.1.16"
+ resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.16.tgz#7d548eb9b83ff0988d102be71f271ca8f9c82a95"
+ integrity sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==
+
graceful-fs@^4.1.2, graceful-fs@^4.1.6:
version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
@@ -1315,6 +1325,14 @@ node-releases@^2.0.19:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
+notistack@^3.0.1:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/notistack/-/notistack-3.0.2.tgz#009799c3fccddeffac58565ba1657d27616dfabd"
+ integrity sha512-0R+/arLYbK5Hh7mEfR2adt0tyXJcCC9KkA2hc56FeWik2QN6Bm/S4uW+BjzDARsJth5u06nTjelSw/VSnB1YEA==
+ dependencies:
+ clsx "^1.1.0"
+ goober "^2.0.33"
+
object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
diff --git a/openmetadata-ui/src/main/resources/ui/package.json b/openmetadata-ui/src/main/resources/ui/package.json
index ccfbadda608..6e11ad6451b 100644
--- a/openmetadata-ui/src/main/resources/ui/package.json
+++ b/openmetadata-ui/src/main/resources/ui/package.json
@@ -109,6 +109,7 @@
"katex": "^0.16.21",
"lodash": "^4.17.21",
"luxon": "^3.2.1",
+ "notistack": "^3.0.2",
"oidc-client": "^1.11.5",
"process": "^0.11.10",
"qs": "6.10.3",
diff --git a/openmetadata-ui/src/main/resources/ui/src/App.tsx b/openmetadata-ui/src/main/resources/ui/src/App.tsx
index 8aef8237640..e51c98acb3c 100644
--- a/openmetadata-ui/src/main/resources/ui/src/App.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/App.tsx
@@ -36,7 +36,11 @@ import {
import { getBasePath } from './utils/HistoryUtils';
import { ThemeProvider } from '@mui/material/styles';
-import { createMuiTheme } from '@openmetadata/ui-core-components';
+import {
+ createMuiTheme,
+ SnackbarContent,
+} from '@openmetadata/ui-core-components';
+import { SnackbarProvider } from 'notistack';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DEFAULT_THEME } from './constants/Appearance.constants';
@@ -102,27 +106,42 @@ const App: FC = () => {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx
index 9a020d228c9..fd6774eaccd 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx
@@ -14,6 +14,7 @@
import { Box, Paper, TableContainer, useTheme } from '@mui/material';
import { useForm } from 'antd/lib/form/Form';
import { AxiosError } from 'axios';
+import { useSnackbar } from 'notistack';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ERROR_MESSAGE } from '../../constants/constants';
@@ -23,7 +24,10 @@ import { CreateDomain } from '../../generated/api/domains/createDomain';
import { withPageLayout } from '../../hoc/withPageLayout';
import { addDataProducts } from '../../rest/dataProductAPI';
import { getIsErrorMatch } from '../../utils/CommonUtils';
-import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
+import {
+ showNotistackError,
+ showNotistackSuccess,
+} from '../../utils/NotistackUtils';
import { useDelete } from '../common/atoms/actions/useDelete';
import { useDataProductFilters } from '../common/atoms/domain/ui/useDataProductFilters';
import { useDomainCardTemplates } from '../common/atoms/domain/ui/useDomainCardTemplates';
@@ -45,6 +49,7 @@ const DataProductListPage = () => {
const dataProductListing = useDataProductListingData();
const theme = useTheme();
const { t } = useTranslation();
+ const { enqueueSnackbar } = useSnackbar();
const { permissions } = usePermissionProvider();
const [form] = useForm();
const [isLoading, setIsLoading] = useState(false);
@@ -82,7 +87,8 @@ const DataProductListPage = () => {
setIsLoading(true);
try {
await addDataProducts(formData as CreateDataProduct);
- showSuccessToast(
+ showNotistackSuccess(
+ enqueueSnackbar,
t('server.create-entity-success', {
entity: t('label.data-product'),
})
@@ -91,7 +97,8 @@ const DataProductListPage = () => {
closeDrawer();
dataProductListing.refetch();
} catch (error) {
- showErrorToast(
+ showNotistackError(
+ enqueueSnackbar,
getIsErrorMatch(error as AxiosError, ERROR_MESSAGE.alreadyExist)
? t('server.entity-already-exist', {
entity: t('label.data-product'),
@@ -101,7 +108,8 @@ const DataProductListPage = () => {
: (error as AxiosError),
t('server.add-entity-error', {
entity: t('label.data-product').toLowerCase(),
- })
+ }),
+ { vertical: 'top', horizontal: 'center' }
);
// Keep drawer open on error so user can fix and retry
} finally {
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/AddDomain/AddDomain.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/AddDomain/AddDomain.component.tsx
index 3d00e211144..e7f13dca53a 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/AddDomain/AddDomain.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/AddDomain/AddDomain.component.tsx
@@ -12,6 +12,7 @@
*/
import { Space, Typography } from 'antd';
import { AxiosError } from 'axios';
+import { useSnackbar } from 'notistack';
import { Fragment, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
@@ -23,8 +24,8 @@ import { withPageLayout } from '../../../hoc/withPageLayout';
import { useDomainStore } from '../../../hooks/useDomainStore';
import { addDomains, getDomainList } from '../../../rest/domainAPI';
import { getIsErrorMatch } from '../../../utils/CommonUtils';
+import { showNotistackError } from '../../../utils/NotistackUtils';
import { getDomainPath } from '../../../utils/RouterUtils';
-import { showErrorToast } from '../../../utils/ToastUtils';
import ResizablePanels from '../../common/ResizablePanels/ResizablePanels';
import TitleBreadcrumb from '../../common/TitleBreadcrumb/TitleBreadcrumb.component';
import AddDomainForm from '../AddDomainForm/AddDomainForm.component';
@@ -34,6 +35,7 @@ import './add-domain.less';
const AddDomain = () => {
const { t } = useTranslation();
const navigate = useNavigate();
+ const { enqueueSnackbar } = useSnackbar();
const [isLoading, setIsLoading] = useState(false);
const { updateDomainLoading, updateDomains } = useDomainStore();
@@ -82,7 +84,8 @@ const AddDomain = () => {
refreshDomains();
goToDomain(res.fullyQualifiedName ?? '');
} catch (error) {
- showErrorToast(
+ showNotistackError(
+ enqueueSnackbar,
getIsErrorMatch(error as AxiosError, ERROR_MESSAGE.alreadyExist)
? t('server.entity-already-exist', {
entity: t('label.domain'),
@@ -92,7 +95,8 @@ const AddDomain = () => {
: (error as AxiosError),
t('server.add-entity-error', {
entity: t('label.domain-lowercase'),
- })
+ }),
+ { vertical: 'top', horizontal: 'center' }
);
} finally {
setIsLoading(false);
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx
index b30d2f0369a..b68e3dae785 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx
@@ -27,6 +27,7 @@ import { ItemType } from 'antd/lib/menu/hooks/useItems';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { cloneDeep, isEmpty, isEqual, toString } from 'lodash';
+import { useSnackbar } from 'notistack';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
@@ -78,6 +79,10 @@ import {
import { getEntityName } from '../../../utils/EntityUtils';
import { getEntityVersionByField } from '../../../utils/EntityVersionUtils';
import Fqn from '../../../utils/Fqn';
+import {
+ showNotistackError,
+ showNotistackSuccess,
+} from '../../../utils/NotistackUtils';
import {
DEFAULT_ENTITY_PERMISSION,
getPrioritizedEditPermission,
@@ -91,7 +96,6 @@ import {
escapeESReservedCharacters,
getEncodedFqn,
} from '../../../utils/StringsUtils';
-import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
import { useRequiredParams } from '../../../utils/useRequiredParams';
import { useFormDrawerWithRef } from '../../common/atoms/drawer';
import DeleteWidgetModal from '../../common/DeleteWidget/DeleteWidgetModal';
@@ -118,6 +122,7 @@ const DomainDetailsPage = ({
handleFollowingClick,
}: DomainDetailsPageProps) => {
const { t } = useTranslation();
+ const { enqueueSnackbar } = useSnackbar();
const { getEntityPermission, permissions } = usePermissionProvider();
const navigate = useNavigate();
const { tab: activeTab, version } = useRequiredParams<{
@@ -182,11 +187,13 @@ const DomainDetailsPage = ({
setAssetCount(totalCount);
} catch (error) {
setAssetCount(0);
- showErrorToast(
+ showNotistackError(
+ enqueueSnackbar,
error as AxiosError,
t('server.entity-fetch-error', {
entity: t('label.asset-plural-lowercase'),
- })
+ }),
+ { vertical: 'top', horizontal: 'center' }
);
}
}
@@ -228,7 +235,8 @@ const DomainDetailsPage = ({
domain.fullyQualifiedName ?? '',
];
await addDataProducts(formData as CreateDataProduct);
- showSuccessToast(
+ showNotistackSuccess(
+ enqueueSnackbar,
t('server.create-entity-success', {
entity: t('label.data-product'),
})
@@ -239,7 +247,8 @@ const DomainDetailsPage = ({
onUpdate?.(domain);
closeDataProductDrawer();
} catch (error) {
- showErrorToast(
+ showNotistackError(
+ enqueueSnackbar,
getIsErrorMatch(error as AxiosError, ERROR_MESSAGE.alreadyExist)
? t('server.entity-already-exist', {
entity: t('label.data-product'),
@@ -249,7 +258,8 @@ const DomainDetailsPage = ({
: (error as AxiosError),
t('server.add-entity-error', {
entity: t('label.data-product').toLowerCase(),
- })
+ }),
+ { vertical: 'top', horizontal: 'center' }
);
} finally {
setIsDataProductLoading(false);
@@ -333,7 +343,8 @@ const DomainDetailsPage = ({
try {
(formData as CreateDomain).parent = domain.fullyQualifiedName;
await addDomains(formData as CreateDomain);
- showSuccessToast(
+ showNotistackSuccess(
+ enqueueSnackbar,
t('server.create-entity-success', {
entity: t('label.sub-domain'),
})
@@ -343,7 +354,8 @@ const DomainDetailsPage = ({
handleTabChange(EntityTabs.SUBDOMAINS);
closeSubDomainDrawer();
} catch (error) {
- showErrorToast(
+ showNotistackError(
+ enqueueSnackbar,
getIsErrorMatch(error as AxiosError, ERROR_MESSAGE.alreadyExist)
? t('server.entity-already-exist', {
entity: t('label.sub-domain'),
@@ -353,7 +365,8 @@ const DomainDetailsPage = ({
: (error as AxiosError),
t('server.add-entity-error', {
entity: t('label.sub-domain').toLowerCase(),
- })
+ }),
+ { vertical: 'top', horizontal: 'center' }
);
} finally {
setIsSubDomainLoading(false);
@@ -421,11 +434,13 @@ const DomainDetailsPage = ({
setSubDomainsCount(totalCount);
} catch (error) {
setSubDomainsCount(0);
- showErrorToast(
+ showNotistackError(
+ enqueueSnackbar,
error as AxiosError,
t('server.entity-fetch-error', {
entity: t('label.sub-domain-lowercase'),
- })
+ }),
+ { vertical: 'top', horizontal: 'center' }
);
}
}
@@ -442,7 +457,8 @@ const DomainDetailsPage = ({
await addDomains(data as CreateDomain);
fetchSubDomainsCount();
} catch (error) {
- showErrorToast(
+ showNotistackError(
+ enqueueSnackbar,
getIsErrorMatch(error as AxiosError, ERROR_MESSAGE.alreadyExist)
? t('server.entity-already-exist', {
entity: t('label.sub-domain'),
@@ -452,7 +468,8 @@ const DomainDetailsPage = ({
: (error as AxiosError),
t('server.add-entity-error', {
entity: t('label.sub-domain-lowercase'),
- })
+ }),
+ { vertical: 'top', horizontal: 'center' }
);
} finally {
closeSubDomainDrawer();
@@ -485,11 +502,13 @@ const DomainDetailsPage = ({
setDataProductsCount(res.data.hits.total.value ?? 0);
} catch (error) {
setDataProductsCount(0);
- showErrorToast(
+ showNotistackError(
+ enqueueSnackbar,
error as AxiosError,
t('server.entity-fetch-error', {
entity: t('label.data-product-lowercase'),
- })
+ }),
+ { vertical: 'top', horizontal: 'center' }
);
}
}
@@ -503,7 +522,10 @@ const DomainDetailsPage = ({
);
setDomainPermission(response);
} catch (error) {
- showErrorToast(error as AxiosError);
+ showNotistackError(enqueueSnackbar, error as AxiosError, undefined, {
+ vertical: 'top',
+ horizontal: 'center',
+ });
}
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx
index f1e5d30fbea..a577968f52f 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx
@@ -14,6 +14,7 @@
import { Box, Paper, TableContainer, useTheme } from '@mui/material';
import { useForm } from 'antd/lib/form/Form';
import { AxiosError } from 'axios';
+import { useSnackbar } from 'notistack';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ERROR_MESSAGE } from '../../constants/constants';
@@ -23,7 +24,10 @@ import { CreateDomain } from '../../generated/api/domains/createDomain';
import { withPageLayout } from '../../hoc/withPageLayout';
import { addDomains } from '../../rest/domainAPI';
import { getIsErrorMatch } from '../../utils/CommonUtils';
-import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
+import {
+ showNotistackError,
+ showNotistackSuccess,
+} from '../../utils/NotistackUtils';
import { useDelete } from '../common/atoms/actions/useDelete';
import { useDomainCardTemplates } from '../common/atoms/domain/ui/useDomainCardTemplates';
import { useDomainFilters } from '../common/atoms/domain/ui/useDomainFilters';
@@ -48,6 +52,7 @@ const DomainListPage = () => {
const { permissions } = usePermissionProvider();
const [form] = useForm();
const [isLoading, setIsLoading] = useState(false);
+ const { enqueueSnackbar } = useSnackbar();
// Use the simplified domain filters configuration
const { quickFilters, defaultFilters } = useDomainFilters({
@@ -82,7 +87,8 @@ const DomainListPage = () => {
setIsLoading(true);
try {
await addDomains(formData as CreateDomain);
- showSuccessToast(
+ showNotistackSuccess(
+ enqueueSnackbar,
t('server.create-entity-success', {
entity: t('label.domain'),
})
@@ -91,7 +97,8 @@ const DomainListPage = () => {
closeDrawer();
domainListing.refetch();
} catch (error) {
- showErrorToast(
+ showNotistackError(
+ enqueueSnackbar,
getIsErrorMatch(error as AxiosError, ERROR_MESSAGE.alreadyExist)
? t('server.entity-already-exist', {
entity: t('label.domain'),
@@ -101,7 +108,8 @@ const DomainListPage = () => {
: (error as AxiosError),
t('server.add-entity-error', {
entity: t('label.domain').toLowerCase(),
- })
+ }),
+ { vertical: 'top', horizontal: 'center' }
);
} finally {
setIsLoading(false);
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/NotistackUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/NotistackUtils.ts
new file mode 100644
index 00000000000..1a49c48176e
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/NotistackUtils.ts
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { AxiosError } from 'axios';
+import { isString } from 'lodash';
+import { VariantType } from 'notistack';
+import { ClientErrors } from '../enums/Axios.enum';
+import i18n from './i18next/LocalUtil';
+import { getErrorText } from './StringsUtils';
+
+/**
+ * Display an error using notistack
+ * @param enqueueSnackbar notistack's enqueueSnackbar function
+ * @param error error text or AxiosError object
+ * @param fallbackText Fallback error message to be displayed
+ * @param anchorOrigin Optional position for the snackbar (defaults to top-right)
+ */
+export const showNotistackError = (
+ enqueueSnackbar: (
+ message: string,
+ options?: {
+ variant?: VariantType;
+ anchorOrigin?: {
+ vertical: 'top' | 'bottom';
+ horizontal: 'left' | 'center' | 'right';
+ };
+ }
+ ) => void,
+ error: AxiosError | string,
+ fallbackText?: string,
+ anchorOrigin?: {
+ vertical: 'top' | 'bottom';
+ horizontal: 'left' | 'center' | 'right';
+ }
+) => {
+ let errorMessage: string;
+
+ if (isString(error)) {
+ errorMessage = error.toString();
+ } else if ('config' in error && 'response' in error) {
+ const method = error.config?.method?.toUpperCase();
+ const fallback =
+ fallbackText && fallbackText.length > 0
+ ? fallbackText
+ : i18n.t('server.unexpected-error');
+ errorMessage = getErrorText(error, fallback);
+
+ // do not show error toasts for 401
+ // since they will be intercepted and the user will be redirected to the signin page
+ // except for principal domain mismatch errors
+ if (
+ error &&
+ (error.response?.status === ClientErrors.UNAUTHORIZED ||
+ (error.response?.status === ClientErrors.FORBIDDEN &&
+ method === 'GET')) &&
+ !errorMessage.includes('principal domain')
+ ) {
+ return;
+ }
+ } else {
+ errorMessage = fallbackText ?? i18n.t('server.unexpected-error');
+ }
+
+ enqueueSnackbar(errorMessage, {
+ variant: 'error',
+ anchorOrigin: anchorOrigin || { vertical: 'top', horizontal: 'right' },
+ });
+};
+
+/**
+ * Display a success message using notistack
+ * @param enqueueSnackbar notistack's enqueueSnackbar function
+ * @param message success message
+ */
+export const showNotistackSuccess = (
+ enqueueSnackbar: (
+ message: string,
+ options?: { variant?: VariantType }
+ ) => void,
+ message: string
+) => {
+ enqueueSnackbar(message, { variant: 'success' });
+};
+
+/**
+ * Display an info message using notistack
+ * @param enqueueSnackbar notistack's enqueueSnackbar function
+ * @param message info message
+ */
+export const showNotistackInfo = (
+ enqueueSnackbar: (
+ message: string,
+ options?: { variant?: VariantType }
+ ) => void,
+ message: string
+) => {
+ enqueueSnackbar(message, { variant: 'info' });
+};
+
+/**
+ * Display a warning message using notistack
+ * @param enqueueSnackbar notistack's enqueueSnackbar function
+ * @param message warning message
+ */
+export const showNotistackWarning = (
+ enqueueSnackbar: (
+ message: string,
+ options?: { variant?: VariantType }
+ ) => void,
+ message: string
+) => {
+ enqueueSnackbar(message, { variant: 'warning' });
+};
diff --git a/openmetadata-ui/src/main/resources/ui/yarn.lock b/openmetadata-ui/src/main/resources/ui/yarn.lock
index d3414007754..b89b1d501a1 100644
--- a/openmetadata-ui/src/main/resources/ui/yarn.lock
+++ b/openmetadata-ui/src/main/resources/ui/yarn.lock
@@ -5410,6 +5410,11 @@ clone@2.x, clone@^2.1.2:
resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz"
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
+clsx@^1.1.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
+ integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
+
clsx@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz"
@@ -7540,6 +7545,11 @@ globrex@^0.1.2:
resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
+goober@^2.0.33:
+ version "2.1.16"
+ resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.16.tgz#7d548eb9b83ff0988d102be71f271ca8f9c82a95"
+ integrity sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==
+
gopd@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz"
@@ -9590,6 +9600,14 @@ normalize-range@^0.1.2:
resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz"
integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
+notistack@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/notistack/-/notistack-3.0.2.tgz#009799c3fccddeffac58565ba1657d27616dfabd"
+ integrity sha512-0R+/arLYbK5Hh7mEfR2adt0tyXJcCC9KkA2hc56FeWik2QN6Bm/S4uW+BjzDARsJth5u06nTjelSw/VSnB1YEA==
+ dependencies:
+ clsx "^1.1.0"
+ goober "^2.0.33"
+
npm-run-path@^4.0.0, npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"