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"