diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ApiServiceRest.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ApiServiceRest.spec.ts index 0f66d1dd1f0..2e5dd23ba6b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ApiServiceRest.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ApiServiceRest.spec.ts @@ -12,7 +12,12 @@ */ import { expect, test } from '@playwright/test'; import { GlobalSettingOptions } from '../../constant/settings'; -import { descriptionBox, redirectToHomePage, uuid } from '../../utils/common'; +import { + descriptionBox, + redirectToHomePage, + toastNotification, + uuid, +} from '../../utils/common'; import { settingClick } from '../../utils/sidebar'; const apiServiceConfig = { @@ -89,10 +94,6 @@ test.describe('API service', () => { await deleteResponse; - await expect(page.locator('.Toastify__toast-body')).toHaveText( - /deleted successfully!/ - ); - - await page.click('.Toastify__close-button'); + await toastNotification(page, /deleted successfully!/); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Policies.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Policies.spec.ts index 5ae25ec71fe..aaace3380ac 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Policies.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Policies.spec.ts @@ -25,7 +25,11 @@ import { UPDATED_RULE_NAME, } from '../../constant/permission'; import { GlobalSettingOptions } from '../../constant/settings'; -import { descriptionBox, redirectToHomePage } from '../../utils/common'; +import { + descriptionBox, + redirectToHomePage, + toastNotification, +} from '../../utils/common'; import { validateFormNameFieldInput } from '../../utils/form'; import { settingClick } from '../../utils/sidebar'; @@ -258,7 +262,8 @@ test.describe('Policy page should work properly', () => { await page.locator('[data-testid="delete-rule"]').click(); // Validate the error message - await expect(page.locator('.Toastify__toast-body')).toContainText( + await toastNotification( + page, ERROR_MESSAGE_VALIDATION.lastRuleCannotBeRemoved ); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Roles.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Roles.spec.ts index ce8df842364..87d724812d4 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Roles.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Roles.spec.ts @@ -12,7 +12,12 @@ */ import { expect, test } from '@playwright/test'; import { GlobalSettingOptions } from '../../constant/settings'; -import { descriptionBox, redirectToHomePage, uuid } from '../../utils/common'; +import { + descriptionBox, + redirectToHomePage, + toastNotification, + uuid, +} from '../../utils/common'; import { removePolicyFromRole } from '../../utils/roles'; import { settingClick } from '../../utils/sidebar'; @@ -201,9 +206,11 @@ test('Roles page should work properly', async ({ page }) => { // Removing the last policy and validating the error message await removePolicyFromRole(page, policies.dataConsumerPolicy, roleName); - await expect(page.locator('.Toastify__toast-body')).toContainText( + await toastNotification( + page, errorMessageValidation.lastPolicyCannotBeRemoved ); + await expect(page.locator('.ant-table-row')).toContainText( policies.dataConsumerPolicy ); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts index 2fc799eadfc..2cd3f8f82d8 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts @@ -26,6 +26,7 @@ import { createNewPage, descriptionBoxReadOnly, redirectToHomePage, + toastNotification, } from '../../utils/common'; import { addMultiOwner, assignTier } from '../../utils/entity'; @@ -237,11 +238,7 @@ entities.forEach((EntityClass) => { await deleteResponse; - await expect(page.locator('.Toastify__toast-body')).toHaveText( - /deleted successfully!/ - ); - - await page.click('.Toastify__close-button'); + await toastNotification(page, /deleted successfully!/); await page.reload(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/ServiceEntityVersionPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/ServiceEntityVersionPage.spec.ts index c7df9756dd9..35bed8e1580 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/ServiceEntityVersionPage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/ServiceEntityVersionPage.spec.ts @@ -27,6 +27,7 @@ import { createNewPage, descriptionBoxReadOnly, redirectToHomePage, + toastNotification, } from '../../utils/common'; import { addMultiOwner, assignTier } from '../../utils/entity'; @@ -202,11 +203,7 @@ entities.forEach((EntityClass) => { await deleteResponse; - await expect(page.locator('.Toastify__toast-body')).toHaveText( - /deleted successfully!/ - ); - - await page.click('.Toastify__close-button'); + await toastNotification(page, /deleted successfully!/); await page.reload(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index 01a7a17f01c..f9806afb0c2 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -116,15 +116,13 @@ export const toastNotification = async ( page: Page, message: string | RegExp ) => { - await expect( - page.locator('.Toastify__toast-body[role="alert"]').first() - ).toHaveText(message); + await page.waitForSelector('[data-testid="alert-bar"]', { state: 'visible' }); - await page - .locator('.Toastify__toast') - .getByLabel('close', { exact: true }) - .first() - .click(); + await expect(page.getByTestId('alert-bar')).toHaveText(message); + + await expect(page.getByTestId('alert-icon')).toBeVisible(); + + await expect(page.getByTestId('alert-icon-close')).toBeVisible(); }; export const clickOutside = async (page: Page) => { @@ -268,13 +266,9 @@ export const replaceAllSpacialCharWith_ = (text: string) => { // This error toast blocks the buttons at the top // Below logic closes the alert if it's present to avoid flakiness in tests export const closeFirstPopupAlert = async (page: Page) => { - const toastLocator = '.Toastify__toast-body[role="alert"]'; - const toastElement = await page.$(toastLocator); - if (toastElement) { - await page - .locator('.Toastify__toast') - .getByLabel('close', { exact: true }) - .first() - .click(); + const toastElement = page.getByTestId('alert-bar'); + + if ((await toastElement.count()) > 0) { + await page.getByTestId('alert-icon-close').first().click(); } }; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/customMetric.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/customMetric.ts index 4ec599c05c2..a1febd08a26 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/customMetric.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/customMetric.ts @@ -16,6 +16,7 @@ import { NAME_MAX_LENGTH_VALIDATION_ERROR, NAME_VALIDATION_ERROR, } from '../constant/common'; +import { toastNotification } from './common'; type CustomMetricDetails = { page: Page; @@ -118,12 +119,11 @@ export const createCustomMetric = async ({ await page.click('[data-testid="submit-button"]'); await createMetricResponse; - await expect(page.locator('.Toastify__toast-body')).toHaveText( + await toastNotification( + page, new RegExp(`${metric.name} created successfully.`) ); - await page.locator('.Toastify__close-button').click(); - // verify the created custom metric await expect(page).toHaveURL(/profiler/); await expect( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts index 900a0a076f5..4d5eb618beb 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts @@ -24,7 +24,12 @@ import { } from '../constant/delete'; import { ES_RESERVED_CHARACTERS } from '../constant/entity'; import { EntityTypeEndpoint } from '../support/entity/Entity.interface'; -import { clickOutside, descriptionBox, redirectToHomePage } from './common'; +import { + clickOutside, + descriptionBox, + redirectToHomePage, + toastNotification, +} from './common'; export const visitEntityPage = async (data: { page: Page; @@ -792,7 +797,8 @@ const announcementForm = async ( ); await page.click('#announcement-submit'); await announcementSubmit; - await page.click('.Toastify__close-button'); + await page.click('[data-testid="announcement-close"]'); + await page.click('[data-testid="alert-icon-close"]'); }; export const createAnnouncement = async ( @@ -1181,11 +1187,7 @@ export const restoreEntity = async (page: Page) => { await page.click('[data-testid="restore-button"]'); await page.click('button:has-text("Restore")'); - await expect(page.locator('.Toastify__toast-body')).toHaveText( - /restored successfully/ - ); - - await page.click('.Toastify__close-button'); + await toastNotification(page, /restored successfully/); const exists = await page .locator('[data-testid="deleted-badge"]') @@ -1224,11 +1226,7 @@ export const softDeleteEntity = async ( await deleteResponse; - await expect(page.locator('.Toastify__toast-body')).toHaveText( - /deleted successfully!/ - ); - - await page.click('.Toastify__close-button'); + await toastNotification(page, /deleted successfully!/); await page.reload(); @@ -1293,11 +1291,7 @@ export const hardDeleteEntity = async ( await page.click('[data-testid="confirm-button"]'); await deleteResponse; - await expect(page.locator('.Toastify__toast-body')).toHaveText( - /deleted successfully!/ - ); - - await page.click('.Toastify__close-button'); + await toastNotification(page, /deleted successfully!/); }; export const checkDataAssetWidget = async (page: Page, serviceType: string) => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts index 8a6d184398a..972ce6ec6dc 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts @@ -156,11 +156,7 @@ export const softDeleteUserProfilePage = async ( await deleteResponse; - await expect(page.locator('.Toastify__toast-body')).toHaveText( - /deleted successfully!/ - ); - - await page.click('.Toastify__close-button'); + await toastNotification(page, /deleted successfully!/); await deletedUserChecks(page); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/App.tsx b/openmetadata-ui/src/main/resources/ui/src/App.tsx index a038ba039a3..56135d4430b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/App.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/App.tsx @@ -16,15 +16,12 @@ import React, { FC, useEffect } from 'react'; import { HelmetProvider } from 'react-helmet-async'; import { I18nextProvider } from 'react-i18next'; import { Router } from 'react-router-dom'; -import { ToastContainer } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.min.css'; import AppRouter from './components/AppRouter/AppRouter'; import { AuthProvider } from './components/Auth/AuthProviders/AuthProvider'; import ErrorBoundary from './components/common/ErrorBoundary/ErrorBoundary'; import { EntityExportModalProvider } from './components/Entity/EntityExportModalProvider/EntityExportModalProvider.component'; import ApplicationsProvider from './components/Settings/Applications/ApplicationsProvider/ApplicationsProvider'; import WebAnalyticsProvider from './components/WebAnalytics/WebAnalyticsProvider'; -import { TOAST_OPTIONS } from './constants/Toasts.constants'; import AntDConfigProvider from './context/AntDConfigProvider/AntDConfigProvider'; import PermissionProvider from './context/PermissionProvider/PermissionProvider'; import TourProvider from './context/TourProvider/TourProvider'; @@ -100,7 +97,6 @@ const App: FC = () => { - ); diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-cross.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-cross.svg new file mode 100644 index 00000000000..4edb4b8312e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-cross.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-error.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-error.svg new file mode 100644 index 00000000000..b321359d7d3 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-error.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Toasts.constants.ts b/openmetadata-ui/src/main/resources/ui/src/components/AlertBar/AlertBar.interface.ts similarity index 72% rename from openmetadata-ui/src/main/resources/ui/src/constants/Toasts.constants.ts rename to openmetadata-ui/src/main/resources/ui/src/components/AlertBar/AlertBar.interface.ts index 33a2e24018d..1a6778f06f1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Toasts.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/AlertBar/AlertBar.interface.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 Collate. + * 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 @@ -10,12 +10,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { AlertProps } from 'antd'; -import { ToastOptions } from 'react-toastify'; - -export const TOAST_OPTIONS: ToastOptions = { - autoClose: false, - hideProgressBar: true, - draggable: false, - closeOnClick: false, -}; +export interface AlertBarProps { + type: AlertProps['type']; + message: string | JSX.Element; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AlertBar/AlertBar.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AlertBar/AlertBar.test.tsx new file mode 100644 index 00000000000..27fc180dd3c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/AlertBar/AlertBar.test.tsx @@ -0,0 +1,75 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; +import { ReactComponent as CrossIcon } from '../../assets/svg/ic-cross.svg'; +import * as ToastUtils from '../../utils/ToastUtils'; +import AlertBar from './AlertBar'; + +const mockResetAlert = jest.fn(); + +jest.mock('../../hooks/useAlertStore', () => ({ + useAlertStore: jest.fn().mockImplementation(() => ({ + resetAlert: mockResetAlert, + animationClass: 'test-animation-class', + })), +})); + +jest.mock('../../utils/ToastUtils', () => ({ + getIconAndClassName: jest.fn(), +})); + +describe('AlertBar', () => { + (ToastUtils.getIconAndClassName as jest.Mock).mockReturnValue({ + icon: CrossIcon, + className: 'test-class', + }); + + it('should render AlertBar with the correct type and message', () => { + const message = 'Test message'; + const type = 'success'; + + render(); + + const alertElement = screen.getByTestId('alert-bar'); + + expect(alertElement).toBeInTheDocument(); + expect(alertElement).toHaveClass( + 'alert-container test-class test-animation-class' + ); + expect(screen.getByTestId('alert-icon')).toBeInTheDocument(); + expect(screen.getByText(message)).toBeInTheDocument(); + }); + + it('should render the CrossIcon as the close button', () => { + const message = 'Test message'; + const type = 'info'; + + render(); + + const closeIcon = screen.getByTestId('alert-icon-close'); + + expect(closeIcon).toBeInTheDocument(); + }); + + it('should apply the correct animation class', () => { + const message = 'Test message'; + const type = 'warning'; + + render(); + + const alertElement = screen.getByTestId('alert-bar'); + + expect(alertElement).toHaveClass('test-animation-class'); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AlertBar/AlertBar.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AlertBar/AlertBar.tsx new file mode 100644 index 00000000000..c6371ceec65 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/AlertBar/AlertBar.tsx @@ -0,0 +1,67 @@ +/* + * 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 { Alert } from 'antd'; +import classNames from 'classnames'; +import React, { useMemo, useState } from 'react'; +import { ReactComponent as CrossIcon } from '../../assets/svg/ic-cross.svg'; +import { useAlertStore } from '../../hooks/useAlertStore'; +import { getIconAndClassName } from '../../utils/ToastUtils'; +import './alert-bar.style.less'; +import { AlertBarProps } from './AlertBar.interface'; + +const AlertBar = ({ type, message }: AlertBarProps): JSX.Element => { + const { resetAlert, animationClass } = useAlertStore(); + const [expanded, setExpanded] = useState(false); + + const { icon: AlertIcon, className } = useMemo(() => { + return getIconAndClassName(type); + }, [type]); + + return ( + + } + data-testid="alert-bar" + description={ + <> + + {message} + + {typeof message === 'string' && message.length > 400 && ( + + )} + + } + icon={AlertIcon && } + type={type} + /> + ); +}; + +export default AlertBar; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AlertBar/alert-bar.style.less b/openmetadata-ui/src/main/resources/ui/src/components/AlertBar/alert-bar.style.less new file mode 100644 index 00000000000..ce724753068 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/AlertBar/alert-bar.style.less @@ -0,0 +1,113 @@ +/* + * 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 (reference) url('../../styles/variables.less'); + +@keyframes resize-show-animation { + from { + height: 0px; + padding: 0 20px; + } + to { + height: max-content; + padding: 16px 20px; + } +} + +@keyframes resize-hide-animation { + from { + height: max-content; + padding: 16px 20px; + } + to { + height: 0px; + padding: 0 20px; + } +} + +.alert-container { + backdrop-filter: blur(500px); + height: 0px; + + .alert-message { + display: -webkit-box; + -webkit-line-clamp: 2; /* Limits to 2 lines */ + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + max-height: 3em; /* Approx height for 2 lines */ + transition: max-height 0.3s ease; + } + + .alert-message.expanded { + -webkit-line-clamp: unset; + max-height: none; + } + + .alert-toggle-btn { + background: none; + border: none; + cursor: pointer; + padding: 0; + font-size: 14px; + text-decoration: underline; + } + + &.show-alert { + animation: resize-show-animation 0.3s ease-in-out forwards; + } + + &.hide-alert { + animation: resize-hide-animation 0.2s ease-in-out forwards; + } + + &.info { + background-color: @info-bg-color; + color: @info-color; + border: none; + border-radius: 0px; + } + + &.success { + background-color: @success-bg-color; + color: @success-color; + border: none; + border-radius: 0px; + } + + &.warning { + background-color: @warning-bg-color; + color: @warning-color; + border: none; + border-radius: 0px; + } + + &.error { + background-color: @error-bg-color; + color: @error-color; + border: none; + border-radius: 0px; + } + + .ant-alert-description { + font-size: 14px; + } + + .alert-close-icon { + margin-right: 6px; + display: flex; + align-items: center; + margin-top: 3px; + width: 14px; + height: 14px; + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/MsalAuthenticator.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/MsalAuthenticator.tsx index dfc9efb7579..c3687226679 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/MsalAuthenticator.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/MsalAuthenticator.tsx @@ -24,13 +24,13 @@ import React, { useEffect, useImperativeHandle, } from 'react'; -import { toast } from 'react-toastify'; import { msalLoginRequest, parseMSALResponse, } from '../../../utils/AuthProvider.util'; import { getPopupSettingLink } from '../../../utils/BrowserUtils'; import { Transi18next } from '../../../utils/CommonUtils'; +import { showErrorToast } from '../../../utils/ToastUtils'; import Loader from '../../common/Loader/Loader'; import { AuthenticatorRef, @@ -96,7 +96,7 @@ const MsalAuthenticator = forwardRef( // eslint-disable-next-line no-console console.error(e); if (e?.message?.includes('popup_window_error')) { - toast.error( + showErrorToast( { ]; }, [permissions]); - const handleAddDomainClick = () => { + const handleAddDomainClick = useCallback(() => { history.push(ROUTES.ADD_DOMAIN); - }; + }, [history]); const handleDomainUpdate = async (updatedData: Domain) => { if (activeDomain) { @@ -182,49 +183,55 @@ const DomainPage = () => { if (!(viewBasicDomainPermission || viewAllDomainPermission)) { return ( - +
+ +
); } if (isEmpty(rootDomains)) { return ( - - {t('message.domains-not-configured')} - +
+ + {t('message.domains-not-configured')} + +
); } return ( - , - }} - pageTitle={t('label.domain')} - secondPanel={{ - children: domainPageRender, - className: 'content-resizable-panel-container p-t-sm', - minWidth: 800, - flex: 0.87, - }} - /> +
+ , + }} + pageTitle={t('label.domain')} + secondPanel={{ + children: domainPageRender, + className: 'content-resizable-panel-container p-t-sm', + minWidth: 800, + flex: 0.87, + }} + /> +
); }; -export default DomainPage; +export default withPageLayout('domain')(DomainPage); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/PageLayoutV1/PageLayoutV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/PageLayoutV1/PageLayoutV1.tsx index 3d03682e20e..156498e73c7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/PageLayoutV1/PageLayoutV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/PageLayoutV1/PageLayoutV1.tsx @@ -21,6 +21,8 @@ import React, { ReactNode, useMemo, } from 'react'; +import { useAlertStore } from '../../hooks/useAlertStore'; +import AlertBar from '../AlertBar/AlertBar'; import DocumentTitle from '../common/DocumentTitle/DocumentTitle'; import './../../styles/layout/page-layout.less'; @@ -60,6 +62,8 @@ const PageLayoutV1: FC = ({ mainContainerClassName = '', pageContainerStyle = {}, }: PageLayoutProp) => { + const { alert } = useAlertStore(); + const contentWidth = useMemo(() => { if (leftPanel && rightPanel) { return `calc(100% - ${leftPanelWidth + rightPanelWidth}px)`; @@ -99,27 +103,40 @@ const PageLayoutV1: FC = ({ {leftPanel} )} - - {children} - - {rightPanel && ( + - {rightPanel} + className={classNames( + `page-layout-v1-center page-layout-v1-vertical-scroll ${ + !alert && 'p-t-sm' + }`, + { + 'flex justify-center': center, + }, + mainContainerClassName + )} + flex={contentWidth} + offset={center ? 3 : 0} + span={center ? 18 : 24}> + + + {alert && ( + + )} + + + {children} + + - )} + {rightPanel && ( + + {rightPanel} + + )} + ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementDrawer/AnnouncementDrawer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementDrawer/AnnouncementDrawer.tsx index b05a59dbaf9..3fbb796815d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementDrawer/AnnouncementDrawer.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementDrawer/AnnouncementDrawer.tsx @@ -57,7 +57,7 @@ const AnnouncementDrawer: FC = ({ {t('label.announcement-plural')} - + ); diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/api/docStore/createDocument.ts b/openmetadata-ui/src/main/resources/ui/src/generated/api/docStore/createDocument.ts index 6bdda84376b..38a962a47d9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/api/docStore/createDocument.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/api/docStore/createDocument.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 Collate. + * 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 @@ -10,9 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - - - /** +/** * This schema defines Document. A Generic entity to capture any kind of Json Payload. */ export interface CreateDocument { diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/api/tests/createTestCaseResult.ts b/openmetadata-ui/src/main/resources/ui/src/generated/api/tests/createTestCaseResult.ts index a34a653597f..9dc61fd339a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/api/tests/createTestCaseResult.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/api/tests/createTestCaseResult.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 Collate. + * 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 diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/configuration/external/automator/addCustomProperties.ts b/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/configuration/external/automator/addCustomProperties.ts index 870b52590ce..d9d46514dbc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/configuration/external/automator/addCustomProperties.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/configuration/external/automator/addCustomProperties.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 Collate. + * 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 diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/configuration/external/automator/removeCustomPropertiesAction.ts b/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/configuration/external/automator/removeCustomPropertiesAction.ts index 11b4815a42f..584ca2e5945 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/configuration/external/automator/removeCustomPropertiesAction.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/configuration/external/automator/removeCustomPropertiesAction.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 Collate. + * 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 diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/configuration/external/automatorAppConfig.ts b/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/configuration/external/automatorAppConfig.ts index 04e5e21d326..4d9e53688ee 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/configuration/external/automatorAppConfig.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/configuration/external/automatorAppConfig.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 Collate. + * 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 diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/type/entityHierarchy.ts b/openmetadata-ui/src/main/resources/ui/src/generated/type/entityHierarchy.ts index 50e64689993..27b4527065a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/type/entityHierarchy.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/type/entityHierarchy.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 Collate. + * 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 diff --git a/openmetadata-ui/src/main/resources/ui/src/hoc/withPageLayout.tsx b/openmetadata-ui/src/main/resources/ui/src/hoc/withPageLayout.tsx new file mode 100644 index 00000000000..0034738d973 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/hoc/withPageLayout.tsx @@ -0,0 +1,31 @@ +/* + * 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 React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import PageLayoutV1 from '../components/PageLayoutV1/PageLayoutV1'; + +export const withPageLayout = + (pageTitleKey: string) => + (Component: FC

) => { + const WrappedComponent: FC

= (props) => { + const { t } = useTranslation(); + + return ( + + + + ); + }; + + return WrappedComponent; + }; diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useAlertStore.test.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useAlertStore.test.ts new file mode 100644 index 00000000000..08c6bbb14be --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useAlertStore.test.ts @@ -0,0 +1,56 @@ +/* + * 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 { act } from '@testing-library/react'; +import { AlertType, useAlertStore } from './useAlertStore'; + +describe('useAlertStore', () => { + it('should update the alert state when addAlert is called', () => { + const { alert, animationClass, addAlert } = useAlertStore.getState(); + + expect(alert).toBeUndefined(); + expect(animationClass).toBe(''); + + const testAlert: AlertType = { + type: 'error', + message: 'Test error message', + }; + + act(() => { + addAlert(testAlert); + }); + + expect(useAlertStore.getState().alert).toEqual(testAlert); + expect(useAlertStore.getState().animationClass).toBe('show-alert'); + }); + + it('should reset the alert state when resetAlert is called', () => { + const { resetAlert, addAlert } = useAlertStore.getState(); + + const testAlert: AlertType = { + type: 'info', + message: 'Test info message', + }; + + act(() => { + addAlert(testAlert); + }); + + expect(useAlertStore.getState().alert).toEqual(testAlert); + + act(() => { + resetAlert(); + }); + + expect(useAlertStore.getState().alert).toBeUndefined(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useAlertStore.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useAlertStore.ts new file mode 100644 index 00000000000..ed12926d3ea --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useAlertStore.ts @@ -0,0 +1,45 @@ +/* + * 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 { AlertProps } from 'antd'; +import { create } from 'zustand'; + +export type AlertType = { + type: AlertProps['type']; + message: string | JSX.Element; +}; + +interface AlertStore { + alert: AlertType | undefined; + animationClass: string; + addAlert: (alert: AlertType, timer?: number) => void; + resetAlert: VoidFunction; +} + +export const useAlertStore = create()((set) => ({ + alert: undefined, + animationClass: '', + addAlert: (alert: AlertType, timer?: number) => { + set({ alert, animationClass: 'show-alert' }); + + const autoCloseTimer = timer ?? (alert.type === 'error' ? Infinity : 5000); + + if (autoCloseTimer !== Infinity) { + setTimeout(() => { + set({ animationClass: 'hide-alert', alert: undefined }); + }, autoCloseTimer); + } + }, + resetAlert: () => { + set({ alert: undefined }); + }, +})); diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json index 6781ee5ad71..ab04e562547 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json @@ -56,6 +56,7 @@ "aggregate": "Aggregate", "airflow-config-plural": "Airflow-Konfigurationen", "alert": "Warnung", + "alert-detail-plural": "Waarschuwingsdetails", "alert-lowercase": "warnung", "alert-lowercase-plural": "warnungen", "alert-plural": "Warnungen", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index ef0b20ff973..b8be3d18676 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -56,6 +56,7 @@ "aggregate": "Aggregate", "airflow-config-plural": "airflow configs", "alert": "Alert", + "alert-detail-plural": "Alert Details", "alert-lowercase": "alert", "alert-lowercase-plural": "alerts", "alert-plural": "Alerts", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json index 282f3a12966..8aa137314ca 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json @@ -56,6 +56,7 @@ "aggregate": "Agregar", "airflow-config-plural": "Configuraciones de airflow", "alert": "Alerta", + "alert-detail-plural": "Detalles de la alerta", "alert-lowercase": "alerta", "alert-lowercase-plural": "alertas", "alert-plural": "Alertas", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index 23863436965..b7989658c2e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -56,6 +56,7 @@ "aggregate": "Aggregate", "airflow-config-plural": "Configurations Airflow", "alert": "Alerte", + "alert-detail-plural": "Détails de l'alerte", "alert-lowercase": "alerte", "alert-lowercase-plural": "alertes", "alert-plural": "Alertes", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json index 3cb9f08c49f..5ee7c795f04 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json @@ -56,6 +56,7 @@ "aggregate": "Agrupar", "airflow-config-plural": "configuracións de Airflow", "alert": "Alerta", + "alert-detail-plural": "Detalles de la alerta", "alert-lowercase": "alerta", "alert-lowercase-plural": "alertas", "alert-plural": "Alertas", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json index 73aaf270864..5ea0361572e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json @@ -56,6 +56,7 @@ "aggregate": "כלול", "airflow-config-plural": "תצורות airflow", "alert": "התראה", + "alert-detail-plural": "פרטי התראה", "alert-lowercase": "התראה", "alert-lowercase-plural": "התראות", "alert-plural": "התראות", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json index 0ca9ed7c94d..9b4d9665b36 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json @@ -56,6 +56,7 @@ "aggregate": "Aggregate", "airflow-config-plural": "Airflowの設定", "alert": "アラート", + "alert-detail-plural": "アラートの詳細", "alert-lowercase": "alert", "alert-lowercase-plural": "alerts", "alert-plural": "アラート", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json index b0ff75e5e78..42c4bb75f6a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json @@ -56,6 +56,7 @@ "aggregate": "एकूण", "airflow-config-plural": "एअरफ्लो संरचना", "alert": "सूचना", + "alert-detail-plural": "सतर्कतेचा तपशील", "alert-lowercase": "सूचना", "alert-lowercase-plural": "सूचना", "alert-plural": "सूचना", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json index 756a56a612d..66c441b2986 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json @@ -56,6 +56,7 @@ "aggregate": "Agregaat", "airflow-config-plural": "Airflowconfiguraties", "alert": "Alert", + "alert-detail-plural": "Alert Details", "alert-lowercase": "alert", "alert-lowercase-plural": "alerts", "alert-plural": "Alerts", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json index 9f819e602c0..34c43a0f6fb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json @@ -56,6 +56,7 @@ "aggregate": "تجمیع", "airflow-config-plural": "پیکربندی‌های ایر‌فلو", "alert": "هشدار", + "alert-detail-plural": "جزئیات هشدار", "alert-lowercase": "هشدار", "alert-lowercase-plural": "هشدارها", "alert-plural": "هشدارها", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json index 388c502721a..0fad9edef08 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json @@ -56,6 +56,7 @@ "aggregate": "Agregado", "airflow-config-plural": "configs do airflow", "alert": "Alerta", + "alert-detail-plural": "Detalhes do Alerta", "alert-lowercase": "alerta", "alert-lowercase-plural": "alertas", "alert-plural": "Alertas", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json index 7a178faf1f0..5caf196e27f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json @@ -56,6 +56,7 @@ "aggregate": "Agregado", "airflow-config-plural": "configs do airflow", "alert": "Alerta", + "alert-detail-plural": "Detalhes do Alerta", "alert-lowercase": "alerta", "alert-lowercase-plural": "alertas", "alert-plural": "Alertas", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json index 8e23a8b303d..2b716804fa7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json @@ -56,6 +56,7 @@ "aggregate": "Aggregate", "airflow-config-plural": "конфиги airflow", "alert": "Предупреждение", + "alert-detail-plural": "Детали оповещения", "alert-lowercase": "предупреждение", "alert-lowercase-plural": "предупреждения", "alert-plural": "Предупреждения", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json index cf89ccbb718..154265bb5b9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json @@ -56,6 +56,7 @@ "aggregate": "รวม", "airflow-config-plural": "การกำหนดค่าของ airflow", "alert": "การแจ้งเตือน", + "alert-detail-plural": "รายละเอียดการแจ้งเตือน", "alert-lowercase": "การแจ้งเตือน", "alert-lowercase-plural": "การแจ้งเตือนหลายอย่าง", "alert-plural": "การแจ้งเตือนหลายอย่าง", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index 827ee011fc1..21d332fdc51 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -56,6 +56,7 @@ "aggregate": "聚合", "airflow-config-plural": "Airflow 配置", "alert": "提醒", + "alert-detail-plural": "警报详情", "alert-lowercase": "提醒", "alert-lowercase-plural": "提醒", "alert-plural": "提醒", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.test.tsx index 0fecec8a14c..123d75cca2d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.test.tsx @@ -84,6 +84,19 @@ jest.mock('../../utils/ToastUtils', () => ({ showErrorToast: jest.fn(), })); +jest.mock('../../hoc/withPageLayout', () => ({ + withPageLayout: jest.fn().mockImplementation( + () => + (Component: React.FC) => + ( + props: JSX.IntrinsicAttributes & { + children?: React.ReactNode | undefined; + } + ) => + + ), +})); + jest.mock( '../../components/Alerts/AlertDetails/AlertConfigDetails/AlertConfigDetails', () => jest.fn().mockImplementation(() =>

AlertConfigDetails
) diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx index 835039f8563..466b2ecfaf4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx @@ -46,6 +46,7 @@ import { EventSubscription, ProviderType, } from '../../generated/events/eventSubscription'; +import { withPageLayout } from '../../hoc/withPageLayout'; import { useFqn } from '../../hooks/useFqn'; import { updateNotificationAlert } from '../../rest/alertsAPI'; import { @@ -450,7 +451,9 @@ function AlertDetailsPage({ minWidth: 700, flex: 0.7, }} - pageTitle={t('label.entity-detail-plural', { entity: t('label.alert') })} + pageTitle={t('label.entity-detail-plural', { + entity: t('label.alert'), + })} secondPanel={{ children: <>, minWidth: 0, @@ -460,4 +463,6 @@ function AlertDetailsPage({ ); } -export default AlertDetailsPage; +export default withPageLayout('alert-detail-plural')( + AlertDetailsPage +); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsightPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsightPage.component.tsx index dcec3ec40c3..23f98bf5104 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsightPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsightPage.component.tsx @@ -13,7 +13,7 @@ import { Col, Row } from 'antd'; import { t } from 'i18next'; -import React, { useLayoutEffect, useMemo, useState } from 'react'; +import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { Redirect, Route, @@ -30,6 +30,7 @@ import { ResourceEntity } from '../../context/PermissionProvider/PermissionProvi import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; import { DataInsightChartType } from '../../generated/dataInsight/dataInsightChartResult'; import { Operation } from '../../generated/entity/policies/policy'; +import { withPageLayout } from '../../hoc/withPageLayout'; import { DataInsightTabs } from '../../interface/data-insight.interface'; import { SystemChartType } from '../../rest/DataInsightAPI'; import { getDataInsightPathWithFqn } from '../../utils/DataInsightUtils'; @@ -75,16 +76,17 @@ const DataInsightPage = () => { SystemChartType | DataInsightChartType >(); - const handleScrollToChart = ( - chartType: SystemChartType | DataInsightChartType - ) => { - if (ENTITIES_CHARTS.includes(chartType as SystemChartType)) { - history.push(getDataInsightPathWithFqn(DataInsightTabs.DATA_ASSETS)); - } else { - history.push(getDataInsightPathWithFqn(DataInsightTabs.APP_ANALYTICS)); - } - setSelectedChart(chartType); - }; + const handleScrollToChart = useCallback( + (chartType: SystemChartType | DataInsightChartType) => { + if (ENTITIES_CHARTS.includes(chartType as SystemChartType)) { + history.push(getDataInsightPathWithFqn(DataInsightTabs.DATA_ASSETS)); + } else { + history.push(getDataInsightPathWithFqn(DataInsightTabs.APP_ANALYTICS)); + } + setSelectedChart(chartType); + }, + [history] + ); useLayoutEffect(() => { if (selectedChart) { @@ -125,52 +127,53 @@ const DataInsightPage = () => { } return ( - , - }} - pageTitle={t('label.data-insight')} - secondPanel={{ - children: ( - - - {isHeaderVisible && ( +
+ , + }} + pageTitle={t('label.data-insight')} + secondPanel={{ + children: ( + + + {isHeaderVisible && ( + + + + )} - + + {dataInsightTabs.map((tab) => ( + + ))} + + + + - )} - - - {dataInsightTabs.map((tab) => ( - - ))} - - - - - - - - - ), - className: 'content-resizable-panel-container p-t-sm', - minWidth: 800, - flex: 0.87, - }} - /> + + + ), + className: 'content-resizable-panel-container p-t-sm', + minWidth: 800, + flex: 0.87, + }} + /> +
); }; -export default DataInsightPage; +export default withPageLayout('data-insight')(DataInsightPage); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsightPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsightPage.test.tsx index bf326c4e1ec..568c5804bf1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsightPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsightPage.test.tsx @@ -40,6 +40,19 @@ jest.mock('../../components/common/ResizablePanels/ResizableLeftPanels', () => { )); }); +jest.mock('../../hoc/withPageLayout', () => ({ + withPageLayout: jest.fn().mockImplementation( + () => + (Component: React.FC) => + ( + props: JSX.IntrinsicAttributes & { + children?: React.ReactNode | undefined; + } + ) => + + ), +})); + jest.mock('../../utils/DataInsightUtils', () => ({ getDataInsightPathWithFqn: jest.fn().mockReturnValue('/'), })); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityPage.test.tsx index ce6f706cb07..8598c7f21cc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityPage.test.tsx @@ -63,6 +63,19 @@ jest.mock('../../components/common/ResizablePanels/ResizableLeftPanels', () => { )); }); +jest.mock('../../hoc/withPageLayout', () => ({ + withPageLayout: jest.fn().mockImplementation( + () => + (Component: React.FC) => + ( + props: JSX.IntrinsicAttributes & { + children?: React.ReactNode | undefined; + } + ) => + + ), +})); + jest.mock('react-router-dom', () => { return { ...jest.requireActual('react-router-dom'), diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityPage.tsx index 40e5719231d..7fe2cb94a85 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityPage.tsx @@ -25,6 +25,7 @@ import LeftPanelCard from '../../components/common/LeftPanelCard/LeftPanelCard'; import ResizableLeftPanels from '../../components/common/ResizablePanels/ResizableLeftPanels'; import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; import { ROUTES } from '../../constants/constants'; +import { withPageLayout } from '../../hoc/withPageLayout'; import { getDataQualityPagePath } from '../../utils/RouterUtils'; import './data-quality-page.less'; import DataQualityClassBase from './DataQualityClassBase'; @@ -68,77 +69,79 @@ const DataQualityPage = () => { }; return ( - - - - ), - }} - pageTitle="Quality" - secondPanel={{ - children: ( - - - - - {t('label.data-quality')} - - - {t('message.page-sub-header-for-data-quality')} - - - - - {tabDetailsComponent.map((tab) => ( - - ))} +
+ + + + ), + }} + pageTitle="Quality" + secondPanel={{ + children: ( + + + + + {t('label.data-quality')} + + + {t('message.page-sub-header-for-data-quality')} + + + + + {tabDetailsComponent.map((tab) => ( + + ))} - - - - - - - - ), - className: 'content-resizable-panel-container p-t-sm', - minWidth: 800, - flex: 0.87, - }} - /> + + + + + + + + ), + className: 'content-resizable-panel-container p-t-sm', + minWidth: 800, + flex: 0.87, + }} + /> +
); }; -export default DataQualityPage; +export default withPageLayout('quality')(DataQualityPage); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/Glossary/GlossaryPage/GlossaryPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/Glossary/GlossaryPage/GlossaryPage.component.tsx index 43ab0c3de72..d82cd80cce0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/Glossary/GlossaryPage/GlossaryPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/Glossary/GlossaryPage/GlossaryPage.component.tsx @@ -46,6 +46,7 @@ import { EntityAction, TabSpecificField } from '../../../enums/entity.enum'; import { Glossary } from '../../../generated/entity/data/glossary'; import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm'; import { Operation } from '../../../generated/entity/policies/policy'; +import { withPageLayout } from '../../../hoc/withPageLayout'; import { usePaging } from '../../../hooks/paging/usePaging'; import { useElementInView } from '../../../hooks/useElementInView'; import { useFqn } from '../../../hooks/useFqn'; @@ -146,9 +147,9 @@ const GlossaryPage = () => { [permissions, isGlossaryActive] ); - const handleAddGlossaryClick = () => { + const handleAddGlossaryClick = useCallback(() => { history.push(ROUTES.ADD_GLOSSARY); - }; + }, [history]); const fetchGlossaryList = useCallback(async () => { try { @@ -236,7 +237,7 @@ const GlossaryPage = () => { } }, [paging, isInView, isMoreGlossaryLoading, pageSize]); - const fetchGlossaryTermDetails = async () => { + const fetchGlossaryTermDetails = useCallback(async () => { setIsRightPanelLoading(true); try { const response = await getGlossaryTermByFQN(glossaryFqn, { @@ -257,7 +258,7 @@ const GlossaryPage = () => { } finally { setIsRightPanelLoading(false); } - }; + }, [glossaryFqn]); useEffect(() => { setIsRightPanelLoading(true); if (glossaries.length) { @@ -277,22 +278,25 @@ const GlossaryPage = () => { } }, [isGlossaryActive, glossaryFqn, glossaries]); - const updateGlossary = async (updatedData: Glossary) => { - const jsonPatch = compare(activeGlossary as Glossary, updatedData); + const updateGlossary = useCallback( + async (updatedData: Glossary) => { + const jsonPatch = compare(activeGlossary as Glossary, updatedData); - try { - const response = await patchGlossaries(activeGlossary?.id, jsonPatch); + try { + const response = await patchGlossaries(activeGlossary?.id, jsonPatch); - updateActiveGlossary({ ...updatedData, ...response }); + updateActiveGlossary({ ...updatedData, ...response }); - if (activeGlossary?.name !== updatedData.name) { - history.push(getGlossaryPath(response.fullyQualifiedName)); - fetchGlossaryList(); + if (activeGlossary?.name !== updatedData.name) { + history.push(getGlossaryPath(response.fullyQualifiedName)); + fetchGlossaryList(); + } + } catch (error) { + showErrorToast(error as AxiosError); } - } catch (error) { - showErrorToast(error as AxiosError); - } - }; + }, + [activeGlossary, updateActiveGlossary, history, fetchGlossaryList] + ); const updateVote = useCallback( async (data: VotingDataProps) => { @@ -318,35 +322,38 @@ const GlossaryPage = () => { [updateActiveGlossary, activeGlossary] ); - const handleGlossaryDelete = async (id: string) => { - try { - await deleteGlossary(id); - showSuccessToast( - t('server.entity-deleted-successfully', { - entity: t('label.glossary'), - }) - ); - setIsLoading(true); - // check if the glossary available - const updatedGlossaries = glossaries.filter((item) => item.id !== id); - setGlossaries(updatedGlossaries); - const glossaryPath = - updatedGlossaries.length > 0 - ? getGlossaryPath(updatedGlossaries[0].fullyQualifiedName) - : getGlossaryPath(); + const handleGlossaryDelete = useCallback( + async (id: string) => { + try { + await deleteGlossary(id); + showSuccessToast( + t('server.entity-deleted-successfully', { + entity: t('label.glossary'), + }) + ); + setIsLoading(true); + // check if the glossary is available + const updatedGlossaries = glossaries.filter((item) => item.id !== id); + setGlossaries(updatedGlossaries); + const glossaryPath = + updatedGlossaries.length > 0 + ? getGlossaryPath(updatedGlossaries[0].fullyQualifiedName) + : getGlossaryPath(); - history.push(glossaryPath); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.delete-entity-error', { - entity: t('label.glossary'), - }) - ); - } finally { - setIsLoading(false); - } - }; + history.push(glossaryPath); + } catch (error) { + showErrorToast( + error as AxiosError, + t('server.delete-entity-error', { + entity: t('label.glossary'), + }) + ); + } finally { + setIsLoading(false); + } + }, + [glossaries, history] + ); const handleGlossaryTermUpdate = useCallback( async (updatedData: GlossaryTerm) => { @@ -380,33 +387,38 @@ const GlossaryPage = () => { [activeGlossary] ); - const handleGlossaryTermDelete = async (id: string) => { - try { - await deleteGlossaryTerm(id); + const handleGlossaryTermDelete = useCallback( + async (id: string) => { + try { + await deleteGlossaryTerm(id); - showSuccessToast( - t('server.entity-deleted-successfully', { - entity: t('label.glossary-term'), - }) - ); - let fqn; - if (glossaryFqn) { - const fqnArr = Fqn.split(glossaryFqn); - fqnArr.pop(); - fqn = fqnArr.join(FQN_SEPARATOR_CHAR); + showSuccessToast( + t('server.entity-deleted-successfully', { + entity: t('label.glossary-term'), + }) + ); + + let fqn; + if (glossaryFqn) { + const fqnArr = Fqn.split(glossaryFqn); + fqnArr.pop(); + fqn = fqnArr.join(FQN_SEPARATOR_CHAR); + } + + setIsLoading(true); + history.push(getGlossaryPath(fqn)); + fetchGlossaryList(); + } catch (err) { + showErrorToast( + err as AxiosError, + t('server.delete-entity-error', { + entity: t('label.glossary-term'), + }) + ); } - setIsLoading(true); - history.push(getGlossaryPath(fqn)); - fetchGlossaryList(); - } catch (err) { - showErrorToast( - err as AxiosError, - t('server.delete-entity-error', { - entity: t('label.glossary-term'), - }) - ); - } - }; + }, + [glossaryFqn, history, fetchGlossaryList] + ); const handleAssetClick = useCallback( (asset?: EntityDetailsObjectInterface) => { @@ -420,24 +432,30 @@ const GlossaryPage = () => { } if (!(viewBasicGlossaryPermission || viewAllGlossaryPermission)) { - return ; + return ( +
+ +
+ ); } if (glossaries.length === 0 && !isLoading) { return ( - +
+ +
); } @@ -518,7 +536,7 @@ const GlossaryPage = () => { /> ); - return <>{resizableLayout}; + return
{resizableLayout}
; }; -export default GlossaryPage; +export default withPageLayout('glossary')(GlossaryPage); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/Glossary/GlossaryPage/GlossaryPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/Glossary/GlossaryPage/GlossaryPage.test.tsx index 9e96b4b174a..0b9e533901a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/Glossary/GlossaryPage/GlossaryPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/Glossary/GlossaryPage/GlossaryPage.test.tsx @@ -53,6 +53,19 @@ jest.mock('../../../context/PermissionProvider/PermissionProvider', () => { }; }); +jest.mock('../../../hoc/withPageLayout', () => ({ + withPageLayout: jest.fn().mockImplementation( + () => + (Component: React.FC) => + ( + props: JSX.IntrinsicAttributes & { + children?: React.ReactNode | undefined; + } + ) => + + ), +})); + jest.mock('../../../components/Glossary/GlossaryV1.component', () => { return jest.fn().mockImplementation((props) => (
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/LineageConfigPage/LineageConfigPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/LineageConfigPage/LineageConfigPage.tsx index 9eaca4e7bd7..9657f63084e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/LineageConfigPage/LineageConfigPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/LineageConfigPage/LineageConfigPage.tsx @@ -33,6 +33,7 @@ import { LineageSettings, } from '../../generated/configuration/lineageSettings'; import { Settings, SettingType } from '../../generated/settings/settings'; +import { withPageLayout } from '../../hoc/withPageLayout'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { getSettingsByType, @@ -76,7 +77,7 @@ const LineageConfigPage = () => { setActiveField(event.target.id); }, []); - const handleSave = async (values: LineageSettings) => { + const handleSave = useCallback(async (values: LineageSettings) => { try { setIsUpdating(true); @@ -88,6 +89,7 @@ const LineageConfigPage = () => { lineageLayer: values.lineageLayer, }, }; + const { data } = await updateSettingsConfig(configData as Settings); showSuccessToast( t('server.update-entity-success', { @@ -105,7 +107,7 @@ const LineageConfigPage = () => { } finally { setIsUpdating(false); } - }; + }, []); useEffect(() => { fetchSearchConfig(); @@ -116,129 +118,131 @@ const LineageConfigPage = () => { } return ( - - - - - +
+ + + + + - - - {t('label.lineage')} - - - -
- - - + + + {t('label.lineage')} + + + + + + + - - - + + + - - - -
- - - - - - - -
-
- ), - minWidth: 700, - flex: 0.7, - }} - pageTitle={t('label.lineage-config')} - secondPanel={{ - className: 'service-doc-panel content-resizable-panel-container', - minWidth: 400, - flex: 0.3, - children: ( - - ), - }} - /> + + + + + + + + + + + +
+
+ ), + minWidth: 700, + flex: 0.7, + }} + pageTitle={t('label.lineage-config')} + secondPanel={{ + className: 'service-doc-panel content-resizable-panel-container', + minWidth: 400, + flex: 0.3, + children: ( + + ), + }} + /> + ); }; -export default LineageConfigPage; +export default withPageLayout('lineage-config')(LineageConfigPage); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaListPage/PersonaPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaListPage/PersonaPage.test.tsx index 4801d246c69..58d1bd10e3b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaListPage/PersonaPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaListPage/PersonaPage.test.tsx @@ -14,12 +14,21 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; import { getAllPersonas } from '../../../rest/PersonaAPI'; import { PersonaPage } from './PersonaPage'; -jest.mock('../../../components/PageLayoutV1/PageLayoutV1', () => { - return jest.fn().mockImplementation(({ children }) =>
{children}
); -}); jest.mock('../../../components/PageHeader/PageHeader.component', () => { return jest.fn().mockImplementation(() =>
PageHeader.component
); }); +jest.mock('../../../hoc/withPageLayout', () => ({ + withPageLayout: jest.fn().mockImplementation( + () => + (Component: React.FC) => + ( + props: JSX.IntrinsicAttributes & { + children?: React.ReactNode | undefined; + } + ) => + + ), +})); jest.mock( '../../../components/common/TitleBreadcrumb/TitleBreadcrumb.component', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaListPage/PersonaPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaListPage/PersonaPage.tsx index f9d90a85db9..a05cd1a2425 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaListPage/PersonaPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaListPage/PersonaPage.tsx @@ -23,19 +23,19 @@ import { TitleBreadcrumbProps } from '../../../components/common/TitleBreadcrumb import { AddEditPersonaForm } from '../../../components/MyData/Persona/AddEditPersona/AddEditPersona.component'; import { PersonaDetailsCard } from '../../../components/MyData/Persona/PersonaDetailsCard/PersonaDetailsCard'; import PageHeader from '../../../components/PageHeader/PageHeader.component'; -import PageLayoutV1 from '../../../components/PageLayoutV1/PageLayoutV1'; import { GlobalSettingsMenuCategory } from '../../../constants/GlobalSettings.constants'; import { PAGE_HEADERS } from '../../../constants/PageHeaders.constant'; import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; import { TabSpecificField } from '../../../enums/entity.enum'; import { Persona } from '../../../generated/entity/teams/persona'; import { Paging } from '../../../generated/type/paging'; +import { withPageLayout } from '../../../hoc/withPageLayout'; import { useAuth } from '../../../hooks/authHooks'; import { usePaging } from '../../../hooks/paging/usePaging'; import { getAllPersonas } from '../../../rest/PersonaAPI'; import { getSettingPageEntityBreadCrumb } from '../../../utils/GlobalSettingsUtils'; -export const PersonaPage = () => { +const PersonaPageLayout = () => { const { isAdminUser } = useAuth(); const { t } = useTranslation(); @@ -82,9 +82,9 @@ export const PersonaPage = () => { fetchPersonas(); }, [pageSize]); - const handleAddNewPersona = () => { + const handleAddNewPersona = useCallback(() => { setAddEditPersona({} as Persona); - }; + }, []); const errorPlaceHolder = useMemo( () => ( @@ -101,28 +101,28 @@ export const PersonaPage = () => { [isAdminUser] ); - const handlePersonalAddEditCancel = () => { + const handlePersonalAddEditCancel = useCallback(() => { setAddEditPersona(undefined); - }; + }, []); - const handlePersonaAddEditSave = () => { + const handlePersonaAddEditSave = useCallback(() => { handlePersonalAddEditCancel(); fetchPersonas(); - }; + }, [fetchPersonas]); - const handlePersonaPageChange = ({ - currentPage, - cursorType, - }: PagingHandlerParams) => { - handlePageChange(currentPage); - if (cursorType) { - fetchPersonas({ [cursorType]: paging[cursorType] }); - } - }; + const handlePersonaPageChange = useCallback( + ({ currentPage, cursorType }: PagingHandlerParams) => { + handlePageChange(currentPage); + if (cursorType) { + fetchPersonas({ [cursorType]: paging[cursorType] }); + } + }, + [handlePageChange, fetchPersonas, paging] + ); if (isEmpty(persona) && !isLoading) { return ( - <> +
{errorPlaceHolder} {Boolean(addEditPersona) && ( { onSave={handlePersonaAddEditSave} /> )} - +
); } return ( - - + + + + + + + + + + + + + + {isLoading + ? [1, 2, 3].map((key) => ( + + + + + + )) + : persona?.map((persona) => ( + + + + ))} + + {showPagination && ( - - - - - - - - - - - - {isLoading - ? [1, 2, 3].map((key) => ( - - - - - - )) - : persona?.map((persona) => ( - - - - ))} - - {showPagination && ( - - - - )} - {Boolean(addEditPersona) && ( - - )} - - + + )} + {Boolean(addEditPersona) && ( + + )} +
); }; + +export const PersonaPage = withPageLayout('persona-plural')(PersonaPageLayout); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TagsPage/TagsPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TagsPage/TagsPage.test.tsx index bdc6f372016..d380475639d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TagsPage/TagsPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TagsPage/TagsPage.test.tsx @@ -240,6 +240,19 @@ jest.mock('../../components/common/ResizablePanels/ResizableLeftPanels', () => )) ); +jest.mock('../../hoc/withPageLayout', () => ({ + withPageLayout: jest.fn().mockImplementation( + () => + (Component: React.FC) => + ( + props: JSX.IntrinsicAttributes & { + children?: React.ReactNode | undefined; + } + ) => + + ), +})); + jest.mock( '../../components/common/RichTextEditor/RichTextEditorPreviewerV1', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TagsPage/TagsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TagsPage/TagsPage.tsx index 7ff3934464d..8a5efd7a36e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TagsPage/TagsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TagsPage/TagsPage.tsx @@ -50,6 +50,7 @@ import { import { Classification } from '../../generated/entity/classification/classification'; import { Tag } from '../../generated/entity/classification/tag'; import { Operation } from '../../generated/entity/policies/accessControl/rule'; +import { withPageLayout } from '../../hoc/withPageLayout'; import { useFqn } from '../../hooks/useFqn'; import { createClassification, @@ -237,11 +238,11 @@ const TagsPage = () => { } }; - const handleCancel = () => { + const handleCancel = useCallback(() => { setEditTag(undefined); setIsAddingTag(false); setIsAddingClassification(false); - }; + }, []); const handleAfterDeleteAction = useCallback(() => { if (!isUndefined(currentClassification)) { @@ -307,72 +308,75 @@ const TagsPage = () => { /** * It redirects to respective function call based on tag/Classification */ - const handleConfirmClick = async () => { + const handleConfirmClick = useCallback(async () => { if (deleteTags.data?.id) { await handleDeleteTag(deleteTags.data.id); } - }; + }, [deleteTags.data?.id, handleDeleteTag]); - const handleUpdateClassification = async ( - updatedClassification: Classification - ) => { - if (!isUndefined(currentClassification)) { - setIsUpdateLoading(true); + const handleUpdateClassification = useCallback( + async (updatedClassification: Classification) => { + if (!isUndefined(currentClassification)) { + setIsUpdateLoading(true); - const patchData = compare(currentClassification, updatedClassification); - try { - const response = await patchClassification( - currentClassification?.id ?? '', - patchData - ); - setClassifications((prev) => - prev.map((item) => { - if ( - item.fullyQualifiedName === - currentClassification.fullyQualifiedName - ) { - return { - ...item, - ...response, - }; - } + const patchData = compare(currentClassification, updatedClassification); + try { + const response = await patchClassification( + currentClassification?.id ?? '', + patchData + ); - return item; - }) - ); - setCurrentClassification((prev) => ({ ...prev, ...response })); - if ( - currentClassification?.fullyQualifiedName !== - updatedClassification.fullyQualifiedName || - currentClassification?.name !== updatedClassification.name - ) { - history.push(getTagPath(response.fullyQualifiedName)); - } - } catch (error) { - if ( - (error as AxiosError).response?.status === HTTP_STATUS_CODE.CONFLICT - ) { - showErrorToast( - t('server.entity-already-exist', { - entity: t('label.classification'), - entityPlural: t('label.classification-lowercase-plural'), - name: updatedClassification.name, - }) - ); - } else { - showErrorToast( - error as AxiosError, - t('server.entity-updating-error', { - entity: t('label.classification-lowercase'), + setClassifications((prev) => + prev.map((item) => { + if ( + item.fullyQualifiedName === + currentClassification.fullyQualifiedName + ) { + return { + ...item, + ...response, + }; + } + + return item; }) ); + setCurrentClassification((prev) => ({ ...prev, ...response })); + + if ( + currentClassification?.fullyQualifiedName !== + updatedClassification.fullyQualifiedName || + currentClassification?.name !== updatedClassification.name + ) { + history.push(getTagPath(response.fullyQualifiedName)); + } + } catch (error) { + if ( + (error as AxiosError).response?.status === HTTP_STATUS_CODE.CONFLICT + ) { + showErrorToast( + t('server.entity-already-exist', { + entity: t('label.classification'), + entityPlural: t('label.classification-lowercase-plural'), + name: updatedClassification.name, + }) + ); + } else { + showErrorToast( + error as AxiosError, + t('server.entity-updating-error', { + entity: t('label.classification-lowercase'), + }) + ); + } + } finally { + setIsEditClassification(false); + setIsUpdateLoading(false); } - } finally { - setIsEditClassification(false); - setIsUpdateLoading(false); } - } - }; + }, + [currentClassification, history] + ); const handleCreatePrimaryTag = async (data: CreateTag) => { try { @@ -454,37 +458,40 @@ const TagsPage = () => { } }; - const handleActionDeleteTag = (record: Tag) => { - if (currentClassification) { - setDeleteTags({ - data: { - id: record.id as string, - name: record.name, - categoryName: currentClassification?.fullyQualifiedName, - isCategory: false, - status: 'waiting', - }, - state: true, - }); - } - }; + const handleActionDeleteTag = useCallback( + (record: Tag) => { + if (currentClassification) { + setDeleteTags({ + data: { + id: record.id as string, + name: record.name, + categoryName: currentClassification?.fullyQualifiedName, + isCategory: false, + status: 'waiting', + }, + state: true, + }); + } + }, + [currentClassification] + ); - const handleEditTagClick = (selectedTag: Tag) => { + const handleEditTagClick = useCallback((selectedTag: Tag) => { setIsAddingTag(true); setEditTag(selectedTag); - }; + }, []); - const handleAddNewTagClick = () => { + const handleAddNewTagClick = useCallback(() => { setIsAddingTag(true); - }; + }, []); - const handleEditDescriptionClick = () => { + const handleEditDescriptionClick = useCallback(() => { setIsEditClassification(true); - }; + }, []); - const handleCancelEditDescription = () => { + const handleCancelEditDescription = useCallback(() => { setIsEditClassification(false); - }; + }, []); useEffect(() => { if (currentClassification) { @@ -516,21 +523,26 @@ const TagsPage = () => { history.push(getTagPath(category.fullyQualifiedName)); }; - const handleAddTagSubmit = async (data: SubmitProps) => { - const updatedData = omit(data, 'color', 'iconURL'); - const style = { - color: data.color, - iconURL: data.iconURL, - }; - if (editTag) { - await handleUpdatePrimaryTag({ ...editTag, ...updatedData, style }); - } else { - await handleCreatePrimaryTag({ ...updatedData, style }); - } - }; + const handleAddTagSubmit = useCallback( + async (data: SubmitProps) => { + const updatedData = omit(data, 'color', 'iconURL'); + const style = { + color: data.color, + iconURL: data.iconURL, + }; - const handleCancelClassificationDelete = () => + if (editTag) { + await handleUpdatePrimaryTag({ ...editTag, ...updatedData, style }); + } else { + await handleCreatePrimaryTag({ ...updatedData, style }); + } + }, + [editTag, handleUpdatePrimaryTag, handleCreatePrimaryTag] + ); + + const handleCancelClassificationDelete = useCallback(() => { setDeleteTags({ data: undefined, state: false }); + }, []); const leftPanelLayout = useMemo( () => ( @@ -701,87 +713,92 @@ const TagsPage = () => { } return ( - - {isUpdateLoading ? ( - - ) : ( - - )} +
+ + {isUpdateLoading ? ( + + ) : ( + + )} - {/* Classification Form */} - {isAddingClassification && ( - - )} + {/* Classification Form */} + {isAddingClassification && ( + + )} - {/* Tags Form */} - {isAddingTag && ( - - )} + {/* Tags Form */} + {isAddingTag && ( + + )} - - - ), - className: 'content-resizable-panel-container', - minWidth: 800, - flex: 0.87, - }} - /> + + + ), + className: 'content-resizable-panel-container', + minWidth: 800, + flex: 0.87, + }} + /> +
); }; -export default TagsPage; +export default withPageLayout('tag-plural')(TagsPage); diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/app.less b/openmetadata-ui/src/main/resources/ui/src/styles/app.less index 7939f76e97d..68caa8880b5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/app.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/app.less @@ -735,22 +735,6 @@ a[href].link-text-grey, background-color: @body-bg-color; } -/* react toastify */ - -.Toastify__toast-container { - width: 450px; - word-break: break-word; -} - -.Toastify__toast { - overflow-y: auto; - max-height: 80vh; -} - -.Toastify__toast-body { - align-items: flex-start; -} - /* ToastUI Editor style */ .ProseMirror .placeholder { color: @text-grey-muted; diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/layout/page-layout.less b/openmetadata-ui/src/main/resources/ui/src/styles/layout/page-layout.less index 4d2cea492ec..54d77e4ccf2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/layout/page-layout.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/layout/page-layout.less @@ -20,6 +20,12 @@ overflow-x: hidden; } +#page-alert { + position: sticky; + top: 0px; + z-index: 20; +} + .page-layout-v1-left-panel { border: @global-border; border-radius: @border-radius-base; diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/spacing.less b/openmetadata-ui/src/main/resources/ui/src/styles/spacing.less index 108a1cdef54..b7009e58cee 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/spacing.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/spacing.less @@ -173,6 +173,9 @@ .m--t-xss { margin-top: -@margin-xss; } +.m--t-sm { + margin-top: -@margin-sm; +} .m--t-md { margin-top: -@margin-md; } diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/variables.less b/openmetadata-ui/src/main/resources/ui/src/styles/variables.less index e1f685b0f90..211e4b10122 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/variables.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/variables.less @@ -110,6 +110,10 @@ @team-avatar-bg: #0950c51a; @om-navbar-height: ~'var(--ant-navbar-height)'; @sidebar-width: 60px; +@error-bg-color: rgb(from @error-color r g b / 0.1); +@success-bg-color: rgb(from @success-color r g b / 0.1); +@warning-bg-color: rgb(from @warning-color r g b / 0.1); +@info-bg-color: rgb(from @info-color r g b / 0.1); // Sizing @page-height: calc(100vh - @om-navbar-height); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ToastUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ToastUtils.ts index 099c5d6cee1..32f2083435c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ToastUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ToastUtils.ts @@ -11,14 +11,55 @@ * limitations under the License. */ -import { AxiosError, isCancel } from 'axios'; +import { + CheckCircleOutlined, + InfoCircleOutlined, + WarningOutlined, +} from '@ant-design/icons'; +import { AlertProps } from 'antd'; +import { AxiosError } from 'axios'; import { isEmpty, isString } from 'lodash'; import React from 'react'; -import { toast } from 'react-toastify'; +import { ReactComponent as ErrorIcon } from '../assets/svg/ic-error.svg'; import { ClientErrors } from '../enums/Axios.enum'; +import { useAlertStore } from '../hooks/useAlertStore'; import i18n from './i18next/LocalUtil'; import { getErrorText } from './StringsUtils'; +export const getIconAndClassName = (type: AlertProps['type']) => { + switch (type) { + case 'info': + return { + icon: InfoCircleOutlined, + className: 'info', + }; + + case 'success': + return { + icon: CheckCircleOutlined, + className: 'success', + }; + + case 'warning': + return { + icon: WarningOutlined, + className: 'warning', + }; + + case 'error': + return { + icon: ErrorIcon, + className: 'error', + }; + + default: + return { + icon: null, + className: '', + }; + } +}; + export const hashCode = (str: string) => { let hash = 0, i, @@ -42,19 +83,18 @@ export const hashCode = (str: string) => { * @param autoCloseTimer Set the delay in ms to close the toast automatically. */ export const showErrorToast = ( - error: AxiosError | string, + error: AxiosError | string | JSX.Element, fallbackText?: string, autoCloseTimer?: number, - callback?: (value: React.SetStateAction) => void + callback?: (value: React.SetStateAction) => void ) => { - if (isCancel(error)) { - return; - } let errorMessage; - if (isString(error)) { + if (React.isValidElement(error)) { + errorMessage = error; + } else if (isString(error)) { errorMessage = error.toString(); - } else { - const method = (error as AxiosError).config?.method?.toUpperCase(); + } else if ('config' in error && 'response' in error) { + const method = error.config?.method?.toUpperCase(); const fallback = fallbackText && fallbackText.length > 0 ? fallbackText @@ -65,19 +105,20 @@ export const showErrorToast = ( // except for principal domain mismatch errors if ( error && - ((error as AxiosError).response?.status === ClientErrors.UNAUTHORIZED || - ((error as AxiosError).response?.status === ClientErrors.FORBIDDEN && + (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'); } callback && callback(errorMessage); - toast.error(errorMessage, { - toastId: hashCode(errorMessage), - autoClose: autoCloseTimer, - }); + useAlertStore + .getState() + .addAlert({ type: 'error', message: errorMessage }, autoCloseTimer); }; /** @@ -86,9 +127,9 @@ export const showErrorToast = ( * @param autoCloseTimer Set the delay in ms to close the toast automatically. `Default: 5000` */ export const showSuccessToast = (message: string, autoCloseTimer = 5000) => { - toast.success(message, { - autoClose: autoCloseTimer, - }); + useAlertStore + .getState() + .addAlert({ type: 'success', message }, autoCloseTimer); }; /** @@ -97,7 +138,5 @@ export const showSuccessToast = (message: string, autoCloseTimer = 5000) => { * @param autoCloseTimer Set the delay in ms to close the toast automatically. `Default: 5000` */ export const showInfoToast = (message: string, autoCloseTimer = 5000) => { - toast.info(message, { - autoClose: autoCloseTimer, - }); + useAlertStore.getState().addAlert({ type: 'info', message }, autoCloseTimer); };